feat: agent board — share work across independently started agents - #9402
feat: agent board — share work across independently started agents#9402yiliang114 wants to merge 56 commits into
Conversation
|
Re-run at the rebased head. Worth saying up front: the board code itself is byte-identical to what the last round reviewed at Template looks good ✓ Problem: this is a feature, so the bar is motivation rather than reproduction — and the motivation is documented and honestly bounded. The design doc is explicit that this is only the portable storage + CLI layer: no membership, no push, no launcher, no runtime addressing, and the original #8724 Qwen-to-Qwen flow continues elsewhere. It also says plainly that a standalone merge is a maintainer decision. That framing survived several rounds of narrowing and it's the right one. Direction: aligned with the multi-agent work this repo is actively building. Claude Code's CHANGELOG has no "board" concept, but agent-team / subagent / teammate coordination is one of its busiest areas right now. One line there usefully corroborates the layering this PR chose — they fixed "background subagents being unable to reply to a message from an unnamed sibling or parent agent ( Size: 2,329 additions, 0 deletions across 19 files. Breakdown: 1,474 production lines (803 of them under Approach: scope feels right, and the speculative surface from earlier rounds is gone. One honest question rather than a blocker: Risk: no high-risk path matches (0 of the 14 non-test files) — no elevated risk signals. Moving on to code review. 🔍 中文说明在 rebase 后的 head 上重跑。先说一个重要事实:board 代码本身与上一轮在 模板完整 ✓ 问题:这是新功能,标准是动机而非复现——而动机有文档支撑且边界诚实。设计文档明确说明这只是可移植的存储 + CLI 层:无成员、无推送、无启动器、无运行时寻址,原 #8724 的 Qwen-to-Qwen 流程由别处继续推进。文档也直说:单独合入属于维护者决定。这个定位经过多轮收敛,是正确的。 方向:与本仓库正在推进的 multi-agent 方向一致。Claude Code 的 CHANGELOG 没有 "board" 概念,但 agent-team / subagent / teammate 协同是当前最活跃的区域之一。其中一条正好印证了本 PR 的分层选择——他们修复了"后台 subagent 无法回复来自未命名兄弟或父 agent 的消息( 规模:19 个文件,2,329 行新增,0 行删除。拆解:生产代码 1,474 行(其中 803 行位于 方案:范围合理,早期轮次的推测性表面已移除。一个诚实的疑问而非阻塞项: 风险:未命中高风险路径(14 个非测试文件中 0 个)——无升级风险信号。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
Code reviewI wrote down my own proposal before opening the diff — per-board private directories under
Findings below; every line number is against 1 · The contested one — raw
|
| File | What changed |
|---|---|
packages/core/src/agents/team/board-lock.ts |
New. Board/actor name validation, UUID ids with exclusive creation, the two-tier per-item lock, private dir setup, and the shared prune walk. |
packages/core/src/agents/team/board-tasks.ts |
New. Task record schema, parser, create/claim/complete transitions, listing, prune selector. |
packages/core/src/agents/team/asks.ts |
New. Ask record schema, parser, create/answer/decline, read-time TTL settling, listing, prune selector. |
packages/core/src/board.ts |
New. Barrel for the dedicated subpath export — deliberately keeps the board chain out of ACP startup. |
packages/cli/src/commands/board.ts |
New. All eight verbs, stdout drain, wait loop with exit codes 0/2/3/4. |
packages/cli/src/commands/board/context.ts |
New. --board/--as presence + name validation. |
packages/cli/src/commands/board/render.ts |
New. Sanitized human panel and the oneLine helper. |
packages/cli/src/config/config.ts |
3 lines: import, .command(boardCommand), and adding board to the family that exits the process. |
packages/cli/src/cli.ts |
1 line: board <command> in TOP_LEVEL_COMMANDS. |
packages/cli/src/cli.test.ts |
1 line: registers boardCommand in the bootstrap import-boundary map. |
packages/cli/vitest.config.ts |
4 lines: alias for the new core subpath. |
packages/core/package.json |
4 lines: the ./board export entry. |
packages/core/src/agents/team/board-tasks.test.ts |
New, 212 lines. Id uniqueness under concurrency, ownership-before-completion, malformed skip, id/filename mismatch, private modes, unsafe names, prune selector. |
packages/core/src/agents/team/board-items.test.ts |
New, 176 lines. Ask addressing, malformed skip, id mismatch, prune lock re-check, prune-by-id reporting. |
packages/core/src/agents/team/asks.test.ts |
New, 163 lines. Ask parser acceptance and rejection matrix. |
packages/cli/src/commands/board/board-cli.test.ts |
New, 303 lines. Flag validation, exit codes, JSON vs human output, injection neutralization. |
docs/plans/2026-08-18-peer-session-collaboration.md |
New, 213 lines. The design from #9399 — layering, product sequence, non-goals, acceptance gate. |
docs/users/features/agent-board.md |
New, 79 lines. User-facing feature page. |
docs/users/features/_meta.ts |
1 line: nav entry. |
Testing — what evidence this comment carries
This is an unattended CI re-run (GITHUB_EVENT_NAME=issue_comment), so per the skill rules I did not build or execute any PR-derived code. The evidence here is (a) this PR's own CI at the reviewed commit, read through the API, and (b) static reading of the diff. No tmux run — that is local-invocation only.
CI at 7e998c25 — 46 check-runs fetched, all completed, zero failures:
| Check | Conclusion |
|---|---|
Test (ubuntu-latest, Node 22.x) |
✅ success |
Integration Tests (no-AK, No Sandbox) |
✅ success |
Lint & Static (ubuntu-latest, Node 22.x) |
✅ success |
Desktop Shell (ubuntu-22.04) |
✅ success |
Desktop Shell (windows-2022) |
✅ success |
TUI parity snapshots (ink vs opentui) |
✅ success |
OpenTUI no-flicker gate |
✅ success |
web-shell E2E Smoke (ubuntu-latest, Node 22.x) |
✅ success |
Post Coverage Comment (ubuntu-latest, 22.x) |
✅ success |
Secret scan (TruffleHog) |
✅ success |
Dependency CVE audit |
✅ success |
Remind on force-push |
✅ success |
Reading the signal: nothing is pending — the three pull_request workflow runs on this SHA (Qwen Code CI, Security Checks, tui-parity) are all completed/success. Skipped and therefore not evidence either way: Test (macos-latest, Node 22.x), Test (windows-latest, Node 22.x) and Integration Tests (CLI, No Sandbox) — these are merge-group-only lanes in this repo, so their skip is not a gap this PR caused, but it does mean no native macOS or Windows board run exists at this head. The remaining skips are bot-orchestration jobs (authorize, review-pr, takeover-*, etc.), not product CI.
Green CI does not settle this PR's central claim, and I want to be precise about what does. The claim is behavioural — two independently started processes sharing work through files — and a unit suite that mocks @qwen-code/qwen-code-core/board outright (which board-cli.test.ts does, via vi.hoisted) proves the CLI contract, not the storage contract across real processes. Two things already close that gap at this exact head, and neither is mine: @wenshao built the real dist/cli.js and drove separate OS processes against one board tree — 12-way cross-process claim races with exactly one winner per round, 24-way in-process races, 400 ids from 16 concurrent creators with zero collisions, a foreign-runtime process holding the item lock across a prune scan, and a mutation run that killed 14 of 18 mutants. And the prior /verify round verdicted 76cccf2a merge-ready at 116/116 assertions — the board code is byte-identical between that head and this one, so that result still describes the code under review.
Sandboxed lanes, stated plainly rather than as padding: re-running @qwen-code /verify or @qwen-code /tmux on Linux would add little, because the Linux behavioural claim is already substantiated by real-process evidence at this head and the board diff has not changed since the last verify. Not verified, and not closable by either lane: Windows-native behaviour — WINDOWS_DEVICE_NAME and the trailing-dot rule are exercised only as string checks on Linux, cross-process proper-lockfile semantics on NTFS are untested, and SAFE_NAME admits both Demo and demo, which collide on APFS and NTFS. Both lanes run Linux, so that gap needs a native run by whoever merges, or an accepted-risk note. The author has admin, so the full lane applies if a maintainer wants it re-exercised.
中文说明
代码审查
我在打开 diff 前先写下了自己的方案——Storage.getGlobalQwenDir() 下的每 board 私有目录、独占创建的 UUID 条目 id、本仓库已有的两级逐条目锁、显式 --board/--as、原始 --json 搭配净化后的人类输出、区分的等待退出码,以及有上限的读取扇出。本 PR 与该形状高度一致,且有三处比我的草稿更细致:emit()(board.ts:31)在 handler resolve 前把 stdout 落盘,注释也点明了 parseArguments 的 process.exit——我核对了那段代码,该命令族确实执行 process.exit(process.exitCode ?? 0),所以不 drain 的话,大于管道缓冲区的 --json 会被截断却仍返回 0;它还区分了"读端提前关闭"(| head)与真正的写入失败,并且只在没有监听器时才装 stdout error 监听。--timeout 在创建 ask 之前校验,被拒的 flag 不会在 board 上留下孤儿 open ask。读取时的 id 与文件名交叉校验,阻止了被复制或改名的记录以不属于它的 id 被变更。
1 · 有争议的那一项——原始 --json 与 bidi(R10-1)。 这里我要说得很准确,因为当前有两位评审意见相左,而我自己第一遍的判断也过于轻率。机制在该 head 上确实存在,是我自己核实的: emit() 写的是 argv.json ? JSON.stringify(value) : sanitizeTerminalText(human)(board.ts:39),JSON.stringify 只转义 U+0000–U+001F、引号与反斜杠;而 sanitizeTerminalText(textUtils.ts:341-346)跑三趟,第三趟正是剥离 bidi 集合——BIDI_OVERRIDE_CHARS_REGEX = /[\u200e\u200f\u202a-\u202e\u2066-\u2069]/g——注释里点名 Trojan Source 类别与 CVE-2021-42572。也就是说本仓库刻意建了 bidi 防护,emit() 的人类分支装上了它,而一个三元表达式之外的 --json 分支绕开了它。八个子命令的 --json 全走该分支,且 plan 文档自己声明 board 文本是不可信的跨 agent 输入。反方论点同样真实,我也核实了: sessions/ps.ts:104-115(既有代码,非本 PR)在相同威胁模型下对 --json 做了相反选择,注释明确写着 "Deliberately raw ... consumers that RENDER these values in a terminal own the sanitization"。所以本 PR 遵循的是仓库既有约定,而不是凭空漏掉;且这里的人类输出路径确实做了净化,并有钉住 stdout/stderr 上 OSC-52/ANSI 注入的测试。我第一遍错在哪: 我凭 ps.ts 先例就把它写成"正确、不是漏洞",而没先确认 sanitizeTerminalText 是否真的覆盖 bidi。它覆盖。所以这处省略是与仓库自己选择建立的防护之间的真实不一致,不只是风格差异——因此我在这里更正记录,而不是让先前那句结论继续立着。为什么我既不定为阻塞项也不驳回: 有三件事在 diff 之外,不该由我裁决。R10-1 线程锚定在该 head 的活代码行上(isOutdated: false)且被标记为已解决——但 resolvedBy 是作者 @yiliang114 本人,收束论证就是那份 ps.ts 反驳。一个带 [certifies-falsely] 标记的 Critical 由被指控方自己关闭、且该线程上没有维护者署名的认可,无论实质谁对,这都是流程缺口。@wenshao 随后在该 head 批准了 PR——那是维护者的一票,但没有记录在 R10-1 线程上。@qqqys 在同一 head 复审,称 R10-1 是唯一站立的阻塞项,同时明确给出第二条收束路径:由维护者在该线程上署名接受 --json 边界,并声明"人肉看原始 JSON"不是受支持面。所以机制已定,策略有争议。值得一提的是,qqqys 建议的修法成本很低且不牺牲保真:把 bidi/C1 集合在 JSON 字符串内重写为 \uXXXX 转义,JSON.parse 之后文档字节等价,jq 与库消费者看到的数据完全一致,而终端永远收不到活的 override 字符。本仓库已经在用这个技巧——同一个文件里的 escapeAnsiCtrlCodes 就是通过 JSON.stringify(match).slice(1, -1) 转义 ANSI 匹配的。这一点值得与"约定说原始"放在一起权衡,因为它恰好消除了该约定所要保护的那个取舍。
2 · 无上限的读取扇出——真实存在,且修复成本比看上去低。 listBoardTasks(board-tasks.ts:154)与 listAsks(asks.ts:203)都是 Promise.all(files.filter(...).map(get...)),每条记录一次 fs.readFile,无分批。snapshot()(board.ts:78)又把两个集合并行跑,所以 qwen board show 一次扇出 N_tasks + N_asks。@wenshao 用真实构建产物 5/5 复现了 EMFILE:ulimit -n 256(macOS 默认软限制)下 300 条即崩,ulimit -n 1024 下 1500 条即崩,没有部分结果,也没有别的读取途径。补充两点:本仓库已把这个隐患当作已知问题处理——memoryDiscovery.ts:58 与 :256 在 CONCURRENT_LIMIT 下分批,注释原文就是 "Process directories in parallel with concurrency limit to prevent EMFILE errors"(目录 10、文件读取 20)。所以修复不是设计问题,而是把仓库既有惯例应用到两个函数上。积累是设计行为,且有一条路径永远排不空——记录只能通过手动 prune --older-than 离开,而 pruneCollection 捕获解析失败后只告警、不 unlink(board-lock.ts:216 对比 :226 的 unlink)。这符合"针对畸形记录的 mutation 失败且不改写它"的契约,但意味着一条校验失败的外部运行时记录会永远留在 board 上,持续为扇出贡献数量。所以触达上限是持久化存储正常使用的结果,不是边缘情况。
3 · 认领互斥守卫没有测试。 claimBoardTask 在 board-tasks.ts:199 拒绝第二个 actor(already claimed by),在 :196 拒绝已完成的任务(already completed)。我读了 board-tasks.test.ts 中所有 claim 调用点::79 认领一次就继续,:114 与 :148 断言的都是在守卫之前就校验失败的记录,:192/:194 是同一 actor 认领两个不同任务。already claimed 这个字符串在测试文件里根本没出现。@wenshao 通过从产物中删掉守卫证明了它是承重的——之后 12/12 个竞争进程全部成功。按 AGENTS.md,缺失测试属于 Suggestion 而非 Critical,除非未测路径本身就是缺陷;实现是正确的,所以这是覆盖缺口。但它是本 PR 中性价比最高的测试,钉住的正是整个功能赖以成立的性质。:196 与 :199 两个分支目前都未被钉住。
4 · prune 锁内复读的测试没有钉住它名字所声称的东西。 board-items.test.ts:133 名为 re-checks prune eligibility while holding the item lock,但它持有锁、不 await 地启动 pruneAsks、让出一次 setImmediate(:150)、改写记录、再释放。stale-read 变异体是否存活,取决于 pruneCollection 的 readdir 是在这一 tick 之前还是之后 resolve。这个脆弱性我可以静态确认;变异体存活这一结果我取自 @wenshao 的实跑,不自行断言。agent-board.md 宣称的保证("Pruning rechecks each record while holding its lock")在代码里是真实的——他用一个在 prune 扫描期间持有锁的外部进程证明了这点——只是没被测试钉住。他建议的形状(测试自己持锁直到 prune 确实阻塞在上面)是正确的修法。
5 · --owner 写入了真实的 owner 却无人尊重,且未文档化。 board task ... --owner web 创建带 owner: "web" 的 pending 任务(board-tasks.ts:111),但 claimBoardTask 只在 status === 'in_progress' 时拒绝——所以任何 actor 都能直接认领掉一个已预分配的 pending 任务。该选项没有 describe(board.ts:127 是裸的 { type: 'string' }),agent-board.md 里也完全没提。要么文档化为非约束性提示,要么让 claim 尊重它。级别低,但对一个立足点就是"给外部运行时提供稳定契约"的功能来说,这是契约歧义。
6 · TTL 推导出的 timeout 状态从不落盘。 settleAsk(asks.ts:129-131)只在读取时计算 timeout,没有任何地方写回,所以 board show 报告 state: "timeout",而磁盘上的文件会一直停在 state: "open"、settledAt: null。在 Qwen Code 内部没问题;但本 PR 的立足点正是 Codex、shell 脚本、定时任务直接读这些文件,它们每一个都必须自行推导 now >= expiresAt,否则会把过期 ask 当成有效的。agent-board.md 补一句可以收口,落盘状态转换收口得更彻底。
7 · 细节。 prune 要求 --as(board.ts:292)随后丢弃;decline 的 reason 与 answer 从不出现在人类输出里(ask --wait 只打印 declined,renderBoard 两者都不显示),只有 --json 有;所有子命令选项都没有 describe,--help 里只有裸 flag 名。
已核查、不构成 finding: 复用良好(atomicWriteJSON 的 forceMode 我查了签名,正是修复历史过宽权限所需;两级锁与 tasks.ts/mailbox.ts/extension-store.ts 一致;专用 ./board 子路径导出把 board 依赖链挡在 ACP 闭包外;tasks.ts/mailbox.ts 都未导出可复用的锁 helper,所以确实无可导入项,第三份本地拷贝是刻意且在文件头说明过的)。校验充分(parseAsk/parseTask 覆盖 schema、id 与文件名不一致、非法 actor 名、空或超长文本、非有限与乱序时间戳、所有非法状态/结果组合,包括外部运行时可能写入的已结算 timeout 形状;畸形跳过与"拒绝且不改写"两个行为都有回读字节的测试钉住)。路径安全(SAFE_NAME 加上显式的 ./../结尾点/Windows 设备名拒绝,覆盖穿越与 Win32 保留名)。
测试证据
这是无人值守的 CI 重跑(GITHUB_EVENT_NAME=issue_comment),因此按 skill 规则我没有构建或执行任何 PR 派生代码。本条评论的证据是:(a) 通过 API 读取的、本 PR 自己在被审提交上的 CI 结果;(b) 对 diff 的静态阅读。没有 tmux 实跑——那只适用于本地调用。
CI 信号解读:无待完成项——该 SHA 上三个 pull_request workflow run(Qwen Code CI、Security Checks、tui-parity)全部 completed/success。被跳过因而不构成任何方向证据的有:Test (macos-latest, Node 22.x)、Test (windows-latest, Node 22.x)、Integration Tests (CLI, No Sandbox)——它们在本仓库是 merge-group 专用通道,跳过不是本 PR 造成的缺口,但确实意味着该 head 上不存在 macOS 或 Windows 原生 board 运行。其余跳过项是机器人编排任务(authorize、review-pr、takeover-* 等),不是产品 CI。
CI 绿色并不能裁决本 PR 的核心主张,我要把"什么才能裁决"说清楚。该主张是行为性的——两个独立启动的进程通过文件共享工作——而一个把 @qwen-code/qwen-code-core/board 整体 mock 掉的单元测试套件(board-cli.test.ts 正是通过 vi.hoisted 这么做的)证明的是 CLI 契约,不是跨真实进程的存储契约。有两件事已在该 head 上补齐了这个缺口,且都不是我做的:@wenshao 构建了真实的 dist/cli.js,用互相独立的操作系统进程共享同一份 board 目录——12 路跨进程认领竞争每轮恰好 1 个赢家、24 路进程内竞争、16 个并发创建者产出 400 个 id 零碰撞、一个在 prune 扫描期间持有 item 锁的外部运行时进程,以及杀掉 18 个变异体中 14 个的变异测试。另外上一轮 /verify 对 76cccf2a 判定 merge-ready(116/116 断言)——board 代码在该 head 与当前 head 之间逐字节一致,所以那个结果描述的正是当前被审代码。
沙箱通道,直说而非凑数:在 Linux 上重跑 @qwen-code /verify 或 @qwen-code /tmux 收益很小,因为 Linux 行为主张已由该 head 上的真实进程证据支撑,且 board diff 自上次 verify 以来未变。未验证、且两个通道都无法收口的: Windows 原生行为——WINDOWS_DEVICE_NAME 与结尾点规则在 Linux 上只作为字符串校验被执行,NTFS 上的跨进程 proper-lockfile 语义未测,且 SAFE_NAME 同时允许 Demo 与 demo,二者在 APFS 与 NTFS 上会撞同一目录。两个通道都跑 Linux,所以这个缺口需要合并者做一次原生运行,或明确接受该风险并记录。作者有 admin 权限,若维护者希望重跑,完整通道适用。
— Qwen Code · qwen3.8-max-2026-09-02
Reviewed at 7e998c25717c99f66293f314f3b68ac85487c657 · re-run with @qwen-code /triage
|
Confidence: 3/5 — the implementation is sound and I have no simpler alternative to offer; what stops me is a contested Critical and an open sequencing call, both of which belong to a human. Two of the three holds from the last round have cleared. CI is green at this rebased head (46 check-runs, zero failures, nothing pending), and the standalone-merge question the design doc raises now has a maintainer's approval on it — @wenshao built the real What I'm not willing to do is supply the second approval right now, for two specific reasons. One Critical is contested at this exact head, and the thread was closed by the person it was filed against. The The part that makes it not mine to settle: the R10-1 thread is anchored to a live line ( Worth noting, because it changes the cost of the decision: qqqys's first remedy doesn't trade away anything the raw- Second, a live availability bug with its timing left open. To be clear about what I am not saying: this is not a request for changes. I found no correctness defect in the storage contract. The locking, exclusive creation, record validation, atomic writes, prune re-read under lock, and stdout drain all hold up under reading, and the concurrency claims are backed by real-process evidence at this head rather than by my say-so. Both test gaps I flagged are gaps, not defects — the guards they leave unpinned are correct in the code. Nothing here is wrong-headed and nothing needs a redesign. What would move me to approve, all settleable at this same head without a new review round: either remedy on R10-1 (the On routing: the PR carries no labels, so the deterministic owner resolver had no area to match, and the documented latest-human-reviewer fallback resolves to @qqqys — which signs itself as a read-only review agent, so mentioning it alone would reach nobody who can decide. (The resolver's node path was also blocked by permission rules in this environment; I used the documented fallback query.) Since the disagreement is the thing that needs settling, I'm putting it to both people holding it. ⏸️ Deferring to @wenshao and @qqqys — one contested Critical whose thread was closed by the author rather than signed off by a maintainer, and an open before-or-after-merge call on a reproduced 中文说明置信度:3/5 —— 实现是扎实的,我也拿不出更简的替代方案;让我停下来的是一个有争议的 Critical 和一个未定的时序决定,两者都该由人来裁。 上一轮的三项保留已解除两项。CI 在这个 rebase 后的 head 上是绿的(46 个 check-run,0 失败,无待完成项),而设计文档提出的"单独合入"问题现在有了维护者的批准——@wenshao 构建了真实的 我不愿意现在做的是补上第二张批准票,原因有两点,都很具体。 其一,一个 Critical 在该 head 上存在争议,而线程是被它所指控的人关闭的。 让它不该由我裁决的部分是:R10-1 线程锚定在活代码行上( 值得一提的是,因为这改变了决策成本:qqqys 的第一个补救方案并不会牺牲原始 其二,一个在世的可用性 bug,其时序被留空。 要说清楚我没有在说什么:这不是请求修改。我没有在存储契约中发现正确性缺陷。加锁、独占创建、记录校验、原子写入、prune 锁内复读、stdout 排空,读下来都成立,且并发主张由该 head 上的真实进程证据支撑,而不是靠我一句话。我指出的两处测试缺口是缺口而非缺陷——它们未钉住的守卫在代码中是正确的。这里没有任何东西是方向错误的,也没有任何地方需要重新设计。 能让我转为批准的条件,全部可在同一 head 上收口、无需新一轮评审:R10-1 的任一补救( 关于转交对象:本 PR 没有标签,因此确定性 owner 解析器没有可匹配的 area,而文档规定的"最近人类评审者"兜底解析到 @qqqys——它自署为只读评审 agent,所以只提它不会触达任何能决定的人。(解析器的 node 路径在本环境中也被权限规则拦下;我使用了文档规定的兜底查询。)既然需要收口的正是这场分歧,我把它交给持分歧的双方。 ⏸️ 转交 @wenshao 与 @qqqys —— 一个有争议的 Critical(其线程由作者而非维护者署名关闭),以及一个关于 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
|
Closing — this PR is wrong. It landed in #7799 ("feat(cli): Add agent view supervisor runtime", 2026-08-01) as the base of a five-PR series, and four of those are open, non-draft, and were updated today:
Deleting the directory would conflict with three of them directly and strand the fourth. What the verification got wrong. I checked static reachability — no path import, no import by module name, all 87 exported symbols unreferenced, no CLI flag, no build config, and a leaf dependency graph. Every one of those is true, and none of them was the right question. "Nothing in The PR description also asserted that nothing would acquire a consumer later, citing #8869's closure. That was wrong: #8869 was a separate fleet attempt, and closing it says nothing about the #7799–#7803 series, which is the actual owner of this code and is still moving. Apologies to @ZijianZhang989 for the noise. No files were removed from |
Stage 1 of docs/plans/2026-08-18-peer-session-collaboration.md. The board
holds task today; these are its two peers, under the same lock and
atomic-write discipline.
An ask is a question with terminal states — answered, declined, timeout — so
a sender always learns which and can wait, reroute, or escalate. That is what
a plain message cannot offer, and it is why the design has no general-purpose
message.
Timeout settles lazily. expiresAt is written at creation and any reader past
the deadline reports timeout; no sweeper walks the directory. A fetch-based
system has no daemon guaranteed to be running, so the only process that can be
relied on is the one currently asking. The deadline is re-checked under the
lock, since answering a lapsed ask would silently resurrect it.
A decision is something awaiting human authority — approval, acceptance,
adjudication unified, because each needs authority and no agent has more of it
than another. It has no expiry on purpose: silent expiry converts "nobody
looked" into "the system decided", which is the one authority nothing but a
human may hold.
Items land at ~/.qwen/boards/{board}/, the layout 2.8 settles, so they never
need the migration the existing split roots do.
board-lock.ts extracts the two-tier lock rather than adding a third copy of
it; folding tasks.ts and mailbox.ts onto it is a follow-up, kept out so this
change does not also rewrite those paths.
Refs #8724
d300730 to
5b45e0a
Compare
|
Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. 中文请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。 |
Stage 2 — the step where the requirement is actually met. With a command
line over the board, a Codex, a shell script, a scheduled job and a Qwen
session participate on identical terms, because running a command is the one
thing all of them can do.
qwen board show | watch | task | claim | done | ask | answer | decline
| raise | resolve
Verbs are flat rather than nested under a noun — board claim t-3, not board
task claim t-3 — because the id prefix already carries the noun. Two levels
instead of three matters for a surface an agent types every turn.
board watch polls rather than watching the directory: fs.watch differs across
platforms and network filesystems, and a directory this small is not the
bottleneck. The panel leads with what needs a human, then what is blocked on
a peer, then work in flight — a panel that leads with "3 agents working" is a
process monitor, this one leads with what will not move until someone acts.
ask --wait is bounded (30s default) with distinct exit codes, since a foreign
agent running it is blocking its own turn.
fleet up writes no terminal code. tmux already gives panes, per-pane cwd,
keyboard switching, zoom, detach and a server that outlives the client, and
core already wraps its CLI. The command sequences those calls and adds the
one thing tmux cannot know about: a pane showing the board. Agents start via
the command passed at split time, so no keystroke can land before a shell is
ready. --with runs any other binary in its own pane, which is how a foreign
agent joins.
board-tasks.ts is deliberately not agents/team/tasks.ts: that module is Agent
Team's in-session list with dependency edges and an in-process change emitter
its scheduler subscribes to, keyed under a different storage root. Keeping
them apart lets this ship without rewriting that scheduler.
Refs #8724
Without this the feature does not work at all. fleet up starts each pane with QWEN_BOARD in its environment, but a plain session ignored it — never claiming a task, never looking for a question addressed to it, never raising anything for the user. Panes of agents that cannot see each other, and a board pane that stays empty. The section is about when to reach for the board, not a command reference. A model given verbs without boundaries uses ask for things it could read itself, or narrates progress instead of moving a task. So it states that completing a task is how you report, that an ask is for what only another participant can answer, and that anything needing authority is a decision no agent resolves — including itself. It also pins the boundaries the design depends on and that prose would drift away from: no general-purpose message, a named owner is a proposal rather than an assignment, and your own output is not visible to anyone else. Absent unless QWEN_BOARD is set, so the common single-agent prompt pays nothing for it. The CLI now re-exports the env var names from core instead of redeclaring them. Two copies of a constant that must agree is the same failure mode this whole design exists to remove. Refs #8724
Written for someone who wants several agents working side by side, so it opens with the command that produces that and a picture of the result rather than with the model. Three things it states plainly because they are the parts that surprise people: the panel is ordered by what needs you first and hides anything settled; --wait always settles, so a caller never hangs; and nothing is pushed to any participant, which is exactly why a tool we did not write can take part. The limits section is honest about the two that matter — participation is cooperative and cannot be enforced on an agent we did not write, and a named owner is a proposal rather than an assignment.
The audit found the command surface was unreachable and the layout broken, which between them meant nothing in this PR could actually be exercised. Blocker: qwen board and qwen fleet were registered but not in parseArguments' exit list, so every invocation printed its output and then booted a full interactive Qwen session on the same stdin. In a fleet pane that is a TUI takeover; from a tool call it is a hung turn. It also made run()'s exit codes meaningless, so the branching the ask contract promises did not work. Layout: tmux -l sizes the *new* pane, and the new pane was the board — so the board took 70% and sat on the right, the opposite of both the intent and the comment. Splitting N ways by hand also computed -l 100% for the last pane, which tmux rejects, so the default --agents 2 failed on a half-built session. Now the window's own pane is respawned as the board and main-vertical does the arithmetic. Running inside tmux also used the configured session name rather than the one the user is actually in, which fails whenever those differ. Ownership: claiming a completed task silently reopened and reassigned it, and anyone could complete work someone else was mid-way through. Completion now requires ownership; notes and hand-offs stay open to anyone, since those are how a stuck board gets unstuck and neither destroys work. Identity: the participant fallback included the pid, so every invocation was a different participant — claim and done on one task came from two identities and an ask could never be addressed back. Stable per-user now. Also: --ttl on ask, since a recipient that only looks between long turns could not answer within the fixed 15 minutes; --wait reads one item instead of listing the board every 500ms; and the asks header no longer claims the timeout is persisted, because it never was and persisting it would mean a read path that writes.
The audit was right that --with was hollow: it set QWEN_BOARD in a foreign pane, but a Codex never reads that variable and nothing we control can inject into its prompt. The env var alone made heterogeneous participation look supported while leaving the operator to reconstruct the protocol by hand — the exact thing an access layer exists to avoid. qwen board protocol prints the same instructions a Qwen session gets, filled in with the real board and participant name, in a form meant to be pasted. --with panes now run it before exec'ing the command, so the text is already in the pane the agent lives in. This does not make participation enforceable, and the docs say so. It makes it possible without a human reading our source. Also adds show --mine, which narrows tasks and asks to one participant while deliberately keeping decisions: those are the human's, and a view that hides them cannot tell the user what is blocking the board.
…caught The second audit found this branch did not typecheck: fleet.ts passed tmuxListPanes' result — already TmuxPaneInfo[] — back into parseTmuxListPanes. Same failure mode as the round before: a path written against an imagined API and never exercised. Worse was what it did when it ran. Inside tmux, the code asked tmux for "the current window" after creating one. For a non-attached command client that resolves from the inherited $TMUX_PANE — the window that invoked us — so fleet up respawned its own pane and killed itself mid-build. tmuxNewWindow now returns the new window's id via -P -F, which is the only thing that can name the window we just made. Also from the audit: the layout ran out of rows past three or four panes because every split halved the same pane, so it now rebalances after each; re-running outside tmux reused an existing window and destroyed the earlier board, so it always makes a fresh one; tmux failures after verifyTmux escaped as a raw stack trace through the yargs handler, now contained to one line; and show --mine filtered only the human render, making the flag a no-op for --json, which is the branch a foreign agent uses. The --with pane no longer prints the protocol before exec'ing: agent TUIs enter the alternate screen and discard it, so the banner was never readable. The operator is told once, after the layout is up, to paste qwen board protocol in. The docs said the environment variables were enough for a foreign tool to join, which was never true; they now say what actually works, and record --ttl, --mine and the running-session limitation.
The last requirement still unmet. Board awareness came only from QWEN_BOARD, which fleet up sets in each pane's environment — and an environment is fixed at launch. A session that was already deep in a problem when coordination started could never participate, which is precisely the case this design exists for. resolveBoardPromptContext now checks a runtime context before the environment, and /board sets it and refreshes the system instruction, so the board section is present from the next turn. getMainSessionSystemInstruction rebuilds the base prompt on every refresh, so no cache had to be invalidated. /board reports where you are /board <name> [as <who>] /board off The point is that the joining session keeps its context. Briefing a fresh teammate is what spawning already does; bringing in an agent that has been working for an hour is what this is for. Docs record it, and the limits section now says what is actually true — a running session can join, but nothing is pushed to it, so it sees an item the next time it looks.
prune was specified in the design and referenced by a comment in asks.ts for a command that did not exist, so settled items accumulated forever. It is manual rather than scheduled: deleting a record another participant may be mid-read on is a concurrency problem worth not having, and a fetch-based system has no daemon that could be trusted to sweep. It takes the item lock so a concurrent settle cannot be lost between the read and the unlink. decisions.ts claimed no agent-visible tool routes to resolveDecision. That was false — qwen board resolve is on the same CLI the prompt teaches agents to use, so anything with a shell could settle its own decision. Enforcing it needs a surface the agent panes do not carry; until then the code, and now the user docs, say "by agreement" rather than pretending it is an invariant. Two smaller repairs from the audit: the lock map grew one mutex per touched path for the process lifetime, which a long-lived board watch would accumulate indefinitely, and a file removed between taking the lock and reading it surfaced as a raw ENOENT instead of the not-found the onMissing contract promises — reachable now that prune exists. Also names the board pane, which showed the hostname before.
Until now the only participants visible were those who had already owned a
task or been party to an ask. An agent that joined a minute ago could not be
addressed, because nothing knew its name — and `board ask <name>` had no way
to learn one, so callers were left guessing.
participants/{name}.json records who is on a board. It does not duplicate the
machine-wide session registry: that one is keyed by pid and answers what is
alive, this one is keyed by declared name and answers who is here. Liveness is
read from the former, so a record never has to be heartbeated — a crashed
agent stops being listed because its pid is gone.
A name held by a live process is suffixed rather than refused: a caller made
to retry with a different name will either pick badly or give up. A name whose
holder is gone is reclaimed, or a crash loop would exhaust every reasonable
one.
Panes join before the agent starts, and /board joins on the way in, rather
than instructing the model to run it. A participant nobody registered cannot
be addressed, which is too load-bearing to leave to whether an agent follows
its prompt.
kind — interactive | daemon | spawned | foreign — is the field that separates
an agent a leader started, which exists to do what it is given, from an
independent one with its own work, for which an assignment is a proposal.
A session mid-turn reads the board at its turn boundary, but one sitting idle at a prompt has no boundary — so an ask addressed to it would sit untouched until its TTL lapsed. That gap is the strongest argument for push, and it does not need push to close. useBoardPending polls every five seconds: below what a person notices, and one readdir plus a few small reads, the same order as sessions ps. The session still chose the moment and nothing can arrive unbidden, so none of the machinery a delivery path would need comes with it. Decisions lead the indicator and carry the mark — an ask blocks one peer, a decision blocks everyone until a person acts. Deliberately not wired into Footer.tsx. That is a 320-line ternary chain whose rendered output I cannot see from here, and blind edits to unverifiable code produced both of this branch's earlier blockers. The hook and its formatter are tested; wiring is one branch in leftBottomContent and wants a visual check. An unreadable board — removed, permissions changed — drops the indicator rather than interrupting the session.
Completes the idle path. The hook polled but nothing rendered it, so a session sitting at a prompt still had no reason to look. It goes in rightItems, next to sandbox and safe-mode, rather than as a branch in leftBottomContent: the indicator is additive and should not displace the shortcut hint. Warning-coloured because everything it counts is something waiting on someone, and decisions lead — an ask blocks one peer, a decision blocks everyone until a person acts. The previous commit left this out for fear of editing a component whose render could not be seen from here. That was the right caution but the wrong conclusion: Footer.test.tsx renders with ink-testing-library, so the wiring is asserted rather than eyeballed. Three cases — no board, quiet board, counts present — and the existing 34 still pass.
parseAsk rejected state 'timeout' although AskState declares it and settleAsk produces it, so a timeout record written to disk by a foreign runtime sharing the board was silently dropped from listings and treated as missing by getAsk/answerAsk/declineAsk. Accept it with the shape settleAsk emits (settledAt present, no answer or reason payload). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Adds a dedicated asks suite: the expired-open auto-transition to timeout that the CLI exit code 3 relies on, the createAsk from===to refusal, foreign timeout records (valid and malformed), and parse-level rejections exercised through getAsk/listAsks. Also pins pruneBoardTasks, which only prunes completed tasks keyed on updatedAt — a path the pruneAsks tests do not share. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 214 passed · 0 failed · 214 total Flakiness gate: ✅ 5 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:214 通过 · 0 失败 · 214 总计 抖动门:✅ 5 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #9402 Deep Verification (round 4) — feat: agent boardVerdict: 中文 — 判定:✅ 通过 · 可合入(agent 判定)本轮为第四次跟进验证。上轮(head
Previous-round findings at the new head
No other round-3 findings existed. All round-3 measurements were re-run at the new head — none carried by the identical-closure shortcut: the delta commits touch ScopeCentral claim (unchanged): independently started agents (separate CLI processes, declared actors) share tasks and asks through a named filesystem board, with addressee-only settlement, a stable JSON contract, and the A/B: the wiring is load-bearing at the new base tipBase control:
6/6 base cells + 9/9 head cells passed. Delta A/B: the timeout-state fix flips a measured leak (H8)Control:
14/14 head cells + 13/13 control cells passed. The pre-fix behavior is not merely cosmetic: a settled timeout record written by a foreign runtime was invisible to every participant and unprunable forever — the fix restores both visibility and garbage collection. Behavioral harnesses (real processes, isolated
|
| step | harness | result |
|---|---|---|
1. task → claim → done → show across invocations; ownership + steal-claim rejection; --owner hand-off; --as filtering; one-line ids; missing-flag refusal |
H1 | 25/25 |
2. ask addressee-only settlement; --wait exits 0/2/3/4 via real background waiters (incl. two concurrent); settle-after-timeout and double-settle rejected; self-ask rejected with no record; --ttl -5/1e308 and --timeout -5/abc rejected with no orphaned ask; --about validation; multi-line answer intact |
H2 | 32/32 |
| 3. malformed neighbours (broken JSON, schema 99, id-mismatch, broken task, non-UUIDv4 filename) skipped by listing; targeted mutations fail with sha256-identical bytes; prune skips malformed, removes settled healthy items by id | H4 | 17/17 |
| 4. prune-vs-reopen under a real cross-process lock (see next section) | H5 | 19/19 + 17/17 |
| 5. perms 0700/0600; 13 unsafe board names + 4 actor names rejected with no record; 64-char boundary accepted; 8-way concurrent id uniqueness; ESC/BEL sanitized in human output while JSON round-trips raw; 65536/65537-char question boundary | H6 | 32/32 |
| 6. piped output integrity (round-3 D2 regression), real kernel pipes (see below) | H7 | 7/7 + 3/3 control |
H5: re-read-under-lock is load-bearing at the new head
Deterministic cross-process race: a holder process (importing the compiled withItemLock from the tree under test) holds the lock 2.5 s and freshens the expired ask's expiresAt at +1.5 s; prune starts while the lock is held and must block on proper-lockfile retries. Witness 03-prune-race-h5.png, logs logs/h5-{preserve,delete}.assert.json.
| build | rounds | outcome |
|---|---|---|
| shipped (head) | 3 | 3/3 preserved, record still listed open, uncontended prune sanity passes |
| M5 mutant (read-before-lock, compiled) | 3 | 3/3 deleted — the stale pre-lock read deletes the freshened record |
Measurement note: an initial mutant arm at a tighter rewrite schedule (0.9 s) failed to delete in 2 of 3 rounds because the CLI's cold start (~1 s on this shared runner) landed the mutant's early read after the rewrite — a round that does not test the mutant. Re-measured with the round-3 schedule (1.5 s rewrite inside a 2.5 s hold): 3/3 deletions in the first three rounds.
H7: drain fix re-proven with a discriminating control
Witness 05-pipe-integrity-h7.png, logs logs/h7-pipe-{head,buggy}.assert.json. 500 tasks (~100 KB) through a shell pipeline whose reader sleeps 4 s inside a real kernel pipe (a node parent cannot emulate the stall — libuv drains child pipes into internal buffers even with no listeners; measured, both builds finished writing in ~1 s under a "stalled" node reader):
| build | delivered | exit | stderr |
|---|---|---|---|
| head (emit awaits drain) | 99,926 bytes, parses, 500/500 tasks | 0 | clean |
| M6 control (fire-and-forget emit) | exactly 65,536 bytes — one pipe buffer — unparseable, silently | 0 | clean |
head, early-closing reader (| head -c 10) |
n/a | 0 | silent |
The control reproduces round 3's measurement byte-for-byte; the drain-await is what closes it.
Mutation / vacuity matrix at the new head
Unmutated controls green: core board suites 23/23 (11 asks + 5 board-items + 7 board-tasks), cli board suite 18/18. Witness 04-mutation-matrix.png; per-mutant vitest logs embedded in logs/matrix.run.log, structured in logs/matrix.json; every restore verified by clean git status. Kill messages are assertion-level (expected-vs-actual), not compile failures.
| mutant | oracle | verdict |
|---|---|---|
| M1 asks.ts: drop addressee check | core suite | killed — lets only the addressed actor answer or decline red |
| M2 board-lock.ts: drop device-name clause | core suite | killed — rejects unsafe board directory names red |
| M3 board-tasks.ts: rethrow parse errors | core suite | killed — both malformed-record tests red |
| M4 board.ts: drop exit-code 2/3 mapping | cli suite | killed — both mapping cells red (expected undefined to be 2/3) |
| M5 board-lock.ts: reads before lock | core suite ×10 | survived 10/10 — carried coverage gap (finding #1), not dead code |
| M5, compiled into dist | H5 cross-process ×3 | killed 3/3 — cell flips to DELETED |
| M5b pruneCollection unlinks unconditionally | core suite | killed — re-check test red (positive control: the suite can go red) |
M7 board.ts: validate --timeout after createAsk (order swap) |
cli suite | killed — both rejects --timeout … before creating the ask cells red |
| M8 board-lock.ts: prune pushes filename | core suite | killed — reports pruned items by id, not by filename red |
| N1 asks.ts: timeout removed from state list (delta half 1) | core suite | killed — accepts a foreign timeout record with the settled shape red |
| N1b asks.ts: timeout shape branch dropped (delta half 2) | core suite | killed — same test red (falls into the declined catch-all) |
| N2 asks.ts: drop self-ask guard | core suite | killed — refuses an ask addressed to its own actor red |
| N3 asks.ts: disable settleAsk TTL projection | core suite | killed — auto-transitions an expired open ask to timeout on read red |
| N4 board-tasks.ts: prune tasks regardless of status | core suite | killed — prunes only completed tasks, keyed on updatedAt red |
N1–N4 prove the new/expanded tests of 76cccf2a are not vacuous: each assertion that kills a mutant is the one the commit says it added (kill attribution verified against the failing test names). Test names match their fixtures (the --about/boundary shapes are what the titles say). One observation, not a finding: the TTL test's comment claims "the on-disk record keeps its original shape"; the suite asserts the read projection and listing, not the disk bytes — the CLI-level settle-after-timeout cell (H2, already timeout from an on-disk-open record) covers that property behaviorally. No mutant regressed from killed to survived versus round 3.
Findings
- Suggestion (completeness, non-blocking; carried from rounds 1–3, stands): the in-process
re-checks prune eligibility while holding the item locktest still does not pin re-read-under-lock — M5 survives it 10/10 while H5 kills the same mutant 3/3 cross-process. A fixture that would go red is the cross-process variant shipped in this artifact. No shipped-code change needed.
No new findings.
Not covered
- Windows runtime behavior — Linux-only verification; the PR itself marks Windows smoke as pending.
- Per-commit attribution inside the delta — the depth-2 checkout reaches only the merge commit,
HEAD^1, andHEAD^2; neither8a21338cnor4f474e6eexists locally (git rev-list HEAD^1..HEAD^2returns 1, the known shallow-boundary artifact, vs 53 commits in the metadata snapshot). The two delta commits were verified as the aggregate diff plus hunk-level controls. - Base ref note: the metadata snapshot's
baseRefOid(a0096eb5…) differs from the local merge-ref baseHEAD^1(18046830…); per the merge-ref contract the local checkout is authoritative and was used throughout. The base advanced from round 3's57157822to18046830; the A/B above is against the new tip, and the merge itself is proven by construction (the working tree is the merge commit; its effective diff is the 19 PR files, all additive, no conflict edits). - Repo-wide test suite / lint — gates scoped to the affected surface: board suites (23 + 18),
cli.test.ts(76/76, includes the bootstrap registration test pinningboardCommand),tsc --noEmiton core and cli (both clean; liveness proven by a planted type error — exit 2 with the planted line reported, revert — exit 0). The PR's own CI covers the rest. - Bundle entry — harnesses drove
packages/cli/dist/index.js. - Flakiness gate on changed test files is run by the workflow, not this round.
- Two intermediate harness self-failures were diagnosed and corrected before counting (H5 mutant timing; H7 stall emulation) — they are measurement-instrument issues on this slow shared runner, not PR behavior; only the corrected final runs are counted in
assertions.json.
Methodology
One container (node:22-bookworm, shared runner), working tree at refs/pull/9402/merge; npm ci + npm run build completed before the round. The PR's CLI ran as real child processes with a unique QWEN_HOME per harness; wait cells used real background waiter processes settled from separate invocations; the prune race used a lock-holder process importing compiled withItemLock from the tree under test, rewriting at +1.5 s inside a 2.5 s hold so the mutant's early read (~1 s after prune spawn, cold-start measured) provably precedes it. Four control trees: base at HEAD^1, delta-revert at HEAD^2, M5 mutant, M6 mutant — each with a cp -rs symlink farm over the root node_modules, every @qwen-code/* link re-created from its original relative target and readlink -f asserted inside the worktree (e.g. realpath core: /__w/qwen-code/qwen-code/tmp/base-tree/packages/core), package-local and packages/channels/* node_modules linked from the main tree (lockfile unchanged across all arms, no internal links in any package-local dir, so versions are identical and no head code leaks into a control). Compiled markers verified in every control dist before use (3-state vs 4-state list, read-before-lock ordering in pruneCollection, absence of the EPIPE/write-callback emit). Pipe cells used real kernel pipes via bash pipelines with PIPESTATUS. Mutations were applied to head sources by harness/matrix.mjs (exact-string replace, unique-occurrence asserted, restore verified by clean git status). Assertion counts: 194 harness cells (logs/*.assert.json) + 13 encoded matrix expectations + 7 gate checks = 214. Evidence images produced by scripts/verify-capture.mjs; raw logs in logs/.
Flakiness gate log
rounds=5 files=5 skipped=0
file packages/cli/src/cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/cli.test.ts
file packages/cli/src/commands/board/board-cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/board/board-cli.test.ts
file packages/core/src/agents/team/asks.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/asks.test.ts
file packages/core/src/agents/team/board-items.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-items.test.ts
file packages/core/src/agents/team/board-tasks.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-tasks.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/cli/src/cli.test.ts: PPPPP
packages/cli/src/commands/board/board-cli.test.ts: PPPPP
packages/core/src/agents/team/asks.test.ts: PPPPP
packages/core/src/agents/team/board-items.test.ts: PPPPP
packages/core/src/agents/team/board-tasks.test.ts: PPPPP
verdict: pass
summary: 5 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/cli.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 2 · packages/cli/src/cli.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 3 · packages/cli/src/cli.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 4 · packages/cli/src/cli.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 5 · packages/cli/src/cli.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
8 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R13-2 plan doc MVP CLI block stale (omits --ttl/--about, duration placeholders) — already reported (round-6 deferred list at plan doc:97, review 4987526508)
- R13-4 claim-exclusivity and completion-ownership guards untested — already reported (round-6 deferred list at board-tasks.test.ts:75, review 4987526508; round-8 deferred list at board-tasks.ts:198, review 5005527635)
- R13-5 claim/done CLI wiring tests missing — already reported (review 4976317181 summary, 2026-08-19)
- R13-6 show --as identity filter has no test — already reported (round-8 deferred list at board.ts:78, review 5005527635)
- R13-7 task/ask not-found mapping unwitnessed at core level — already reported (round-9 deferred list at board-tasks.ts:183 and asks.ts:234, review 5061677901)
- R13-8 aboutTask is write-only — already reported (round-2 deferred list at asks.ts:63, review 4973213467)
- R13-10 pruneAsks expired-open arm untested — already reported (round-9 deferred list at asks.ts:280, review 5061677901)
- R13-11 backwards wall clock rejects self-produced records — already reported (round-6 deferred list at board-tasks.ts:178 +2 locations, review 4987526508)
Unresolved, please confirm:
- [Critical] packages/cli/src/commands/board.ts:39 — R10-1 (emit() --json branch bypasses sanitizeTerminalText): the mechanism is present at HEAD (JSON.stringify does not strip bidi override/isolate characters; re-traced this round); the author's rebutt…
Not reviewed: reverse audit — stopped at the 5-round cap without two consecutive dry rounds (rounds 1-5 each reported findings).
Not reviewed: build-and-test — Test (ubuntu-latest, Node 22.x) failed in CI at the reviewed commit and the suite did not run locally.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally; the new qwen board CLI surface is exactly what that suite exercises.
Not reviewed: build-and-test — Test (macos-latest / windows-latest, Node 22.x) platform suites were skipped in CI and did not run locally; the board code is filesystem-sensitive (proper-lockfile cross-process locks, name case handling).
Not reviewed: build-and-test — unit suites (packages/core, packages/cli) timed out at their full deadlines under Agent 7 (infrastructure), with failures observed only in files this diff does not touch; the PR's own test files ran green (core board suites 23/23, board-cli 18/18, cli.test.ts 76/76); test-efficacy probes inconclusive (harness not validated); dependent-workspace suites (qwen-live, sdk-typescript, vscode-ide-companion, web-shell, webui) did not run and web-shell E2E Smoke failed in CI.
Deferred under the convergence posture (round 13, not a blocker) — recorded, not requested in this round:
packages/core/src/agents/team/board-tasks.test.ts:204 — [probe] prune test cannot distinguish updatedAt from createdAt keying — the settledAt-key mutation ships greenpackages/core/src/agents/team/board-lock.ts:131 — [probe] warn-only onCompromised lets a stalled writer commit after lock takeover — lost update reported as successpackages/cli/src/commands/board/board-cli.test.ts:95 — [probe] task --owner forwarding has no parse test — dropping owner from the handler ships greenpackages/core/src/agents/team/asks.test.ts:113 — [probe] TTL projection test never pins the on-disk record stays untouched — a write-back on read ships greenpackages/core/src/agents/team/board-items.test.ts:44 — [review] board-items.test.ts named after a nonexistent module; asks.ts coverage split across two files with triplicated mock boilerplatepackages/core/src/agents/team/board-tasks.test.ts:124 — [probe] createBoardTask owner persistence untested — owner:null mutation ships both suites greenpackages/core/src/agents/team/board-tasks.test.ts:203 — [probe] pruneCollection's malformed-record skip untested on the tasks side — removing the catch ships green
Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 8 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未决,请确认:共 1 条(原文未翻译,列表见上方英文部分)。
未审查:reverse audit — stopped at the 5-round cap without two consecutive dry rounds (rounds 1-5 each reported findings)。
未审查:build-and-test — Test (ubuntu-latest, Node 22.x) failed in CI at the reviewed commit and the suite did not run locally。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally; the new qwen board CLI surface is exactly what that suite exercises。
未审查:build-and-test — Test (macos-latest / windows-latest, Node 22.x) platform suites were skipped in CI and did not run locally; the board code is filesystem-sensitive (proper-lockfile cross-process locks, name case handling)。
未审查:build-and-test — unit suites (packages/core, packages/cli) timed out at their full deadlines under Agent 7 (infrastructure), with failures observed only in files this diff does not touch; the PR's own test files ran green (core board suites 23/23, board-cli 18/18, cli.test.ts 76/76); test-efficacy probes inconclusive (harness not validated); dependent-workspace suites (qwen-live, sdk-typescript, vscode-ide-companion, web-shell, webui) did not run and web-shell E2E Smoke failed in CI。
收敛姿态下延后(第 13 轮,非阻断)——已记录,本轮不要求修改:共 7 条(原文未翻译,列表见上方英文部分)。
机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.3)
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 116 passed · 0 failed · 116 total Flakiness gate: ✅ 5 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:116 通过 · 0 失败 · 116 总计 抖动门:✅ 5 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #9402 Deep Verification (round 5) — feat: agent boardVerdict: 中文 — 判定:✅ 通过 · 可合入(agent 判定)本轮为第五次跟进验证。与第四轮相比 head、base、merge 三个提交 OID 逐字节相同(
Previous-round findings at the new headRound 5 is a zero-delta follow-up: the checkout's merge/base/head OIDs are byte-identical to round 4's (
Carried under the identical-closure shortcut (stated because the closure above holds, and each is re-anchored where cheap): round 4's matrix rows M1–M4, M7, M8, N1–N4 (all killed) and the M6 pipe-drain control measurement (buggy build truncates at exactly 65,536 bytes). The head side of the pipe property was re-proven fresh this round (cell F1), and the ScopeCentral claim (unchanged): independently started agents (separate CLI processes, declared actors) share tasks and asks through a named filesystem board, with addressee-only settlement, a stable JSON contract, and the A/B: the wiring is load-bearing (fresh base rebuild)Base control: fresh
7/7 base cells + 10/10 head cells = 17/17 fresh. Delta scenario
|
| probe | result |
|---|---|
| listing | foreign record visible, state: timeout (2 asks listed) |
| human render | foreign question shown |
answer / decline the settled record |
exit 1, already timeout, sha256-identical bytes |
prune --older-than 0 |
removes it by id; healthy open ask survives |
| malformed timeout shapes (no settledAt / with answer / with reason) | all skipped by listing — the fix did not widen acceptance |
Behavioral harnesses (real processes, isolated QWEN_HOME per section)
QWEN_HOME is the global qwen dir itself, so boards land at $QWEN_HOME/boards. Every Reviewer Test Plan step re-executed as scripted cells against node packages/cli/dist/index.js; rerunnable from harness/h1-behavior.mjs. Witness 02-behavior-smoke-h1.png, log logs/h1.assert.json.
| step | cells | result |
|---|---|---|
1. task → claim → done → show across invocations; steal-claim refused with bytes untouched; done-by-non-owner refused; claim-of-completed refused; --owner hand-off; show --as filtering (owner / participant / unrelated); prune removes completed task by id, keeps in-progress |
15 (A) | 15/15 |
2. ask addressee-only settlement (answer and decline refused for non-addressee, bytes untouched); double-settle refused; self-ask refused with no record; --timeout -5/abc and --ttl -5 rejected before createAsk (no orphaned ask, counted); --wait exit codes 0 (answered, real background waiter) / 2 (declined) / 3 (item TTL projected on read) / 4 (local timeout, "still open" on stderr); settle-after-timeout refused from disk, bytes untouched; multi-line answer round-trips |
24 (B) | 24/24 |
| 3. foreign timeout record (section above) | 12 (C) | 12/12 |
| 3b. malformed neighbours: broken JSON / schema 99 / id-mismatch tasks + broken ask + non-matching filename skipped by listing; targeted claims fail with sha256-preserved bytes; prune removes settled healthy items by id and leaves malformed files byte-identical | 10 (D) | 10/10 |
| 5. perms 0700/0600 across root/board/collections/files; 11 unsafe board names + 4 actor names refused with no stray dirs; 64-char boundary accepted; 8-way concurrent creation → 8 unique ids, 8 files; ESC/BEL neutralized in human output while JSON round-trips raw; 65536-char question accepted, 65537 refused | 16 (E) | 16/16 |
pipe integrity (head arm): 500 tasks (~175 KB) through a 4 s-stalled kernel pipe fully delivered and parseable; early-closing reader (head -c 10) exits 0 silently |
2 (F) | 2/2 |
F1 re-proves the head side of round 4's drain measurement fresh (175,430 bytes delivered vs the 65,536-byte truncation the M6 fire-and-forget control produced there); the buggy-control half is carried under the closure shortcut.
H5: re-read-under-lock is load-bearing (fresh race, both builds)
Deterministic cross-process race (harness/h5-race.mjs + h5-holder.mjs): a holder process holds the real proper-lockfile lock on an expired ask for 2.5 s and freshens its expiresAt at +1.5 s; prune starts +0.4 s into the hold and provably blocks on lock retries (pruneMs ≈ 2.2 s ≈ hold minus head-start). Witness 03-prune-race-and-matrix.png; logs logs/h5-*.json.
| build | rounds | outcome |
|---|---|---|
| shipped (head dist, sha256-marked) | 3 | 3/3 PRESERVED, record listed open |
M5 mutant (read-before-lock, compiled; dist marker verified: early read precedes withItemLock) |
3 | 3/3 DELETED — stale pre-lock eligibility deletes the freshened record |
| restored build (dist sha256 byte-identical to shipped marker) | 1 | 1/1 PRESERVED |
Timing note: CLI cold start on this runner is ~0.3 s (faster than round 4's ~1 s), so the mutant's early read lands ~0.7–0.9 s into the hold, comfortably before the +1.5 s rewrite — no ambiguous rounds (round 4's tighter-schedule artifact did not recur).
Mutation re-measurement (finding #1)
Unmutated controls green at this head: core board suites 23/23, cli board 18/18. Mutations applied by harness/matrix.mjs (unique-anchor asserted, backup/restore, clean git status verified after each).
| mutant | oracle | verdict |
|---|---|---|
M5 board-lock.ts: eligibility read before the item lock |
core board suites × 5 | survived 5/5 (carried finding #1 — a coverage gap, not dead code) |
M5b board-lock.ts: unlink unconditionally (positive control) |
core board suites × 1 | killed — board-items.test.ts:160 red: expected [], received ["a-…"]; same file as the mutant, behavioral message, not a compile failure |
| M5, compiled into dist | H5 cross-process × 3 | killed 3/3 |
| M1–M4, M7, M8, N1–N4 (round-4 rows) | — | carried under the proven-identical closure (all killed in round 4; no mutant regressed, none re-tested individually this round) |
Targeted gates
Witness 04-gates.png; raw logs in logs/.
| gate | result | liveness proof |
|---|---|---|
| core board suites (asks + board-items + board-tasks) | 23/23 (3 files) | M5b turns the suite red at the intended assertion |
cli board suite (board-cli.test.ts) |
18/18 | round-4 M4 kill (exit-code mapping) carried; suite re-run green here |
cli bootstrap (cli.test.ts) |
76/76 | includes the boardCommand registration test |
core tsc --noEmit |
clean, exit 0 | planted const now: string = Date.now() in asks.ts → exit 2, names asks.ts(152,9) TS2322; revert → exit 0 |
cli tsc --noEmit |
clean, exit 0 | planted const board: number = … in board.ts → exit 2, names board.ts(113,19) TS2322; revert → exit 0 |
git status clean after every planted check.
Findings
- Suggestion (completeness, non-blocking; carried from rounds 1–4, stands): the in-process
re-checks prune eligibility while holding the item locktest still cannot distinguish re-read-under-lock from read-early-delete-under-lock — M5 survives it 5/5 at this head while the cross-process H5 race kills the same mutant 3/3. A fixture that would pin it is the cross-process harness shipped in this artifact. No shipped-code change needed.
No new findings. No corrections to prior-round statements were needed. (One precision added this round: QWEN_HOME is the global qwen dir — boards land at $QWEN_HOME/boards, not $QWEN_HOME/.qwen/boards; round 4 did not state a layout.)
Not covered
- Windows runtime behavior — Linux-only verification; the PR itself marks Windows smoke as pending.
- Per-commit attribution — depth-2 checkout reaches only the merge commit,
HEAD^1, andHEAD^2;git rev-list HEAD^1..HEAD^2returns 1 (the known shallow-boundary artifact) vs 53 commits in the metadata snapshot. Moot this round (zero delta), carried from round 4's aggregate-diff verification. - Base ref note: the metadata snapshot's
baseRefOid(a0096eb5…) differs from the local merge-ref baseHEAD^1(180468306c…); per the merge-ref contract the local checkout is authoritative and was used throughout (identical to round 4). - Repo-wide test suite / lint — gates scoped to the affected surface (table above); the PR's own CI covers the rest.
- Bundle entry — harnesses drove
packages/cli/dist/index.js. - Flakiness gate on changed test files is run by the workflow, not this round (its round-4 run: 5 files × 5 rounds, no divergence).
- Three intermediate harness self-failures were diagnosed and corrected before counting, all instrument bugs, none PR behavior: (1) a wrong
QWEN_HOMEpath assumption ($QWEN_HOME/.qwen/boardsvs the actual$QWEN_HOME/boards) that crashed two sections and made one H0 cell pass for the wrong reason — H0 was re-run after the fix; (2) an exit-listener attached after the child had already exited, hanging the first H5 attempt; (3) a missingencodingoption. Only corrected final runs are counted inassertions.json.
Methodology
One container (node:22-bookworm, shared loaded runner, Node v22.23.2), working tree at refs/pull/9402/merge; npm ci + npm run build completed before the round. Identity of this round with round 4 was established by comparing the merge/base/head OIDs (7a69aa65…, 180468306c…, 76cccf2a…) against round 4's report values, clean git status, and the lockfile-unchanged diff; the tree hash 7c06b145… is recorded for audit. The PR's CLI ran as real child processes with a unique QWEN_HOME per harness section; wait cells used real background waiter processes settled from separate invocations; the prune race used a lock-holder process importing compiled withItemLock from packages/core/dist (the same dist the CLI resolves, so both arms share one lock implementation), rewriting at +1.5 s inside a 2.5 s hold while prune provably blocked on proper-lockfile retries (prune wall-time ≈ 2.2 s). The base control tree rebuilt only core + cli against a symlink-farm node_modules with internal @qwen-code/{qwen-code-core,qwen-code} links re-pointed into the base tree (readlink -f witnesses quoted above); the mutant arm rebuilt core only, with compiled dist markers checked before use and the restored dist verified sha256-byte-identical to the shipped marker. Assertion counts: 17 (H0 wiring) + 79 (H1 behavioral) + 13 (encoded matrix/race expectations: M5b red ×1, M5 green ×5, H5 shipped ×3, H5 mutant ×3, restore sanity ×1) + 7 (gate checks) = 116. Evidence images produced by scripts/verify-capture.mjs; raw per-cell logs in logs/.
Flakiness gate log
rounds=5 files=5 skipped=0
file packages/cli/src/cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/cli.test.ts
file packages/cli/src/commands/board/board-cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/board/board-cli.test.ts
file packages/core/src/agents/team/asks.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/asks.test.ts
file packages/core/src/agents/team/board-items.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-items.test.ts
file packages/core/src/agents/team/board-tasks.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-tasks.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/cli/src/cli.test.ts: PPPPP
packages/cli/src/commands/board/board-cli.test.ts: PPPPP
packages/core/src/agents/team/asks.test.ts: PPPPP
packages/core/src/agents/team/board-items.test.ts: PPPPP
packages/core/src/agents/team/board-tasks.test.ts: PPPPP
verdict: pass
summary: 5 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/cli.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 2 · packages/cli/src/cli.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 3 · packages/cli/src/cli.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 4 · packages/cli/src/cli.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 5 · packages/cli/src/cli.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
|
@qwen-code /triage |
|
⏸️ Deferring to @wenshao and @qqqys — the code review is clean but two calls are not mine to make at Status since the last ping: both earlier holds cleared. CI is green at this rebased head (46 check-runs, zero failures, nothing pending), and @wenshao's real-process validation of the built What I need a human on:
Non-blocking either way: the claim-exclusivity guard ( Full detail in the Stage 2 comment. Assigned to @wenshao as the approving maintainer. 中文说明⏸️ 转交 @wenshao 与 @qqqys —— 代码审查是干净的,但在 距上次提醒的状态:先前两项保留均已解除。CI 在这个 rebase 后的 head 上是绿的(46 个 check-run,0 失败,无待完成项),@wenshao 对真实构建产物 需要人来裁的两件事:
无论哪种选择都非阻塞:认领互斥守卫( 完整细节见 Stage 2 评论。已指派给 @wenshao 作为批准的维护者。 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
|
Sandboxed verification: The verification job did not complete (checkout, runner, or setup error) and produced no report. See the workflow run for details. 中文 — 判定:
|
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 100 passed · 0 failed · 100 total Flakiness gate: ✅ 5 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:100 通过 · 0 失败 · 100 总计 抖动门:✅ 5 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #9402 Deep Verification (round 6) — feat: agent boardVerdict: 中文 — 判定:✅ 通过 · 可合入(agent 判定)本轮为第六次跟进验证,且再次为零增量:merge/base/head 三个 OID 与第五轮逐字节相同,工作树干净,
Previous-round findings at the new headRound 6 is a zero-delta follow-up: the checkout's merge/base/head OIDs are byte-identical to round 5's (
ScopeCentral claim: independently started agents (separate CLI processes, declared actors) share tasks and asks through a named filesystem board, with addressee-only settlement, a stable JSON contract, and the A/B: the wiring is load-bearing (fresh base rebuild)Base control: fresh
8 base cells + 13 head cells = 21/21 fresh. Behavioral smoke (real CLI processes, isolated
|
| step | cells | result |
|---|---|---|
1. task → claim → done → show; steal-claim refused with bytes untouched; done-by-non-owner refused; claim-of-completed refused; --owner hand-off; show --as filtering (owner / unrelated); prune removes completed by id, keeps in-progress/pending |
11 (A) | 11/11 |
2. ask addressee-only settlement (answer and decline refused for non-addressee, bytes untouched); double-settle refused; self-ask refused with no record; --timeout -5/abc and --ttl -5 rejected before createAsk (no orphaned ask); --wait exit codes 0 (answered, real background waiter) / 2 (declined) / 3 (item TTL projected on read) / 4 (local timeout, "still open" on stderr); settle-after-timeout refused from disk, bytes untouched; multi-line answer round-trips |
16 (B) | 16/16 |
3. foreign timeout-settled record listed with state timeout, human render shows it, answer/decline refused byte-preserved, prune removes it by id while the healthy open ask survives; three malformed timeout shapes (no settledAt / with answer / with reason) skipped byte-preserved |
8 (C) | 8/8 |
| 3b. malformed neighbours: broken JSON / schema 99 / id-mismatch tasks + broken ask skipped by listing; targeted mutations fail with sha256-preserved bytes; non-matching filename ignored; prune removes settled healthy items by id, malformed files byte-identical | 10 (D) | 10/10 |
| 5. perms 0700 across root/board/collections and 0600 records; 11 unsafe board names + 4 actor names refused with no stray dirs; 64-char boundary accepted; 8-way concurrent creation → 8 unique ids, 8 valid files; ESC/BEL neutralized in human output while JSON round-trips raw; 65536-char question accepted, 65537 refused | 8 (E) | 8/8 |
pipe integrity: 500 tasks (102,427 bytes) through a 4 s-stalled kernel pipe fully delivered and parseable (writer provably blocked: pipeline wall 4049–4051 ms); early-closing reader (head -c 10) exits 0 silently |
2 (F) | 2/2 |
Finding #1 re-measurement: re-read-under-lock is load-bearing
Deterministic cross-process race (harness/h5-race.mjs + h5-holder.mjs): a holder process holds the real proper-lockfile lock on an expired ask for 2.5 s and freshens its expiresAt at +1.5 s; prune starts +0.4 s into the hold and provably blocks on lock retries (pruneMs ≈ 2.15–2.2 s). Mutant arm built in a scratch worktree (tmp/mutant-tree, since removed) whose internal @qwen-code/* links were re-pointed into it; the M5 mutation moves the eligibility read before withItemLock (read-early, delete-under-lock), M5b removes the eligibility check entirely (positive control). Witness 03-prune-race-and-matrix.png; logs logs/h5-*.json, /tmp/m5-run-*.log.
| build | oracle | rounds | outcome |
|---|---|---|---|
| shipped (main-tree dist, no M5 marker) | H5 cross-process | 3 (+1 live witness) | PRESERVED, removed: [] |
| M5 mutant (dist marker verified) | H5 cross-process | 3 (+1 live witness) | DELETED — stale pre-lock eligibility removes the freshened record |
| M5 mutant | core board suites (in-process) | 5 | survived 5/5 (23/23 each) — carried finding #1 |
| M5b mutant | core board suites (in-process) | 1 | killed — board-items.test.ts:160 red expected [], received ["a-…"], same file as the mutant |
Targeted gates (fresh runs, liveness proven)
Witness 04-gates.png; raw logs in logs/ (gate-*.log, live-*.log).
| gate | result | liveness proof |
|---|---|---|
| core board suites (asks + board-items + board-tasks) | 23/23 (3 files) | M5b turns the suite red at the intended assertion (board-items.test.ts:160) |
cli board suite (board-cli.test.ts) |
18/18 | planted exit-code flip (declined 2→5) → exactly maps an ask declined outcome to exit code 2 red: expected 5 to be 2 |
cli bootstrap (cli.test.ts) |
76/76 | planted removal of board from TOP_LEVEL_COMMANDS → alignment test red: expected [ 'auth', 'channel', …(7) ] to include 'board' |
core tsc --noEmit |
clean, exit 0 | planted const planted: string = Date.now() in asks.ts → exit 2, names asks.ts(282,9) TS2322 |
cli tsc --noEmit |
clean, exit 0 | planted const planted: number = 'not a number' in board.ts → exit 2, names board.ts(36,9) TS2322 |
git status clean (0 modified files) after every plant; post-restore sha256 of all three planted files byte-identical to the pre-plant hashes recorded in logs/pre-plant.sha.
Findings
- Suggestion (completeness, non-blocking; carried from rounds 1–5, stands — re-measured): the in-process
re-checks prune eligibility while holding the item locktest still cannot distinguish re-read-under-lock from read-early-delete-under-lock — M5 survives it 5/5 at this head while the cross-process H5 race kills the same mutant 3/3. A fixture that would pin it is the cross-process harness shipped in this artifact (harness/h5-race.mjs+h5-holder.mjs). No shipped-code change needed.
No new findings. No corrections to prior-round statements were needed.
Not covered
- Windows runtime behavior — Linux-only verification; the PR itself marks Windows smoke as pending.
- Per-commit attribution — depth-2 checkout reaches only the merge commit,
HEAD^1, andHEAD^2;git rev-list HEAD^1..HEAD^2returns the known shallow-boundary artifact vs 53 commits in the metadata snapshot. Moot this round (zero delta); carried from earlier rounds' aggregate-diff verification. - Base ref note: the metadata snapshot's
baseRefOid(a0096eb5…) differs from the local merge-ref baseHEAD^1(180468306c…); per the merge-ref contract the local checkout is authoritative and was used throughout. - Repo-wide test suite / lint — gates scoped to the affected surface (table above); the PR's own CI covers the rest.
- Bundle entry — harnesses drove
packages/cli/dist/index.js. - Flakiness gate on changed test files is run by the workflow, not this round.
- Two harness instrument bugs were diagnosed and corrected before counting (neither is PR behavior): (1) the first F1 design stalled the pipe from inside the parent process via
stdout.pause(), which does not create kernel backpressure — the child exited at ~1.1 s with 0 bytes captured; it was replaced by a real two-process kernel pipe (harness/slow-reader.mjs), whose final cell measures a genuine 4 s stall (pipeline wall 4049–4051 ms, 102,427 bytes, 500 tasks). (2)verify-captureinjectsFORCE_COLOR, which paired withNO_COLORmakes Node print a warning to the child's stderr and polluted two stderr-quiet cells; harnesses now strip both vars for children, and H1 was re-run at 55/55 under injectedFORCE_COLOR=1. Only corrected final runs are counted inassertions.json.
Methodology
One container (node:22-bookworm, shared loaded runner, Node v22.23.2), working tree at refs/pull/9402/merge. Identity with round 5 established by comparing the merge/base/head OIDs and HEAD^{tree} (7c06b145…) against round 5's recorded values, clean git status, and the lockfile-unchanged diff. The PR's CLI ran as real child processes with a unique QWEN_HOME per harness section; wait cells used real background waiter processes settled from separate invocations; the prune race used a lock-holder process importing proper-lockfile (the same implementation withItemLock uses), rewriting at +1.5 s inside a 2.5 s hold while prune provably blocked on retries. The base control tree rebuilt only core + cli against a symlink-farm node_modules with internal @qwen-code/{qwen-code-core,qwen-code} links re-pointed into the base tree (readlink -f witnesses quoted above); the mutant arm rebuilt core + cli in a scratch worktree with compiled dist markers checked before use. Assertion counts: 21 (H0 wiring) + 55 (H1 behavioral) + 14 (encoded matrix/race expectations: M5b red ×1, M5 green ×5, H5 shipped ×3, H5 mutant ×3, dist markers ×2) + 10 (gate checks: 5 green gates + 5 planted-liveness reds) = 100. Evidence images produced by scripts/verify-capture.mjs; raw per-cell logs in logs/.
Flakiness gate log
rounds=5 files=5 skipped=0
file packages/cli/src/cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/cli.test.ts
file packages/cli/src/commands/board/board-cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/board/board-cli.test.ts
file packages/core/src/agents/team/asks.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/asks.test.ts
file packages/core/src/agents/team/board-items.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-items.test.ts
file packages/core/src/agents/team/board-tasks.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-tasks.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/cli/src/cli.test.ts: PPPPP
packages/cli/src/commands/board/board-cli.test.ts: PPPPP
packages/core/src/agents/team/asks.test.ts: PPPPP
packages/core/src/agents/team/board-items.test.ts: PPPPP
packages/core/src/agents/team/board-tasks.test.ts: PPPPP
verdict: pass
summary: 5 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/cli.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 2 · packages/cli/src/cli.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 3 · packages/cli/src/cli.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 4 · packages/cli/src/cli.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 5 · packages/cli/src/cli.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 405 passed · 0 failed · 405 total Flakiness gate: ✅ 5 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:405 通过 · 0 失败 · 405 总计 抖动门:✅ 5 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #9402 Deep Verification (round 7) — feat: agent boardVerdict: Verified head 中文 — 判定:
|
| # | finding (rounds 1–6) | severity | status at 7e998c25 (round 7) |
|---|---|---|---|
| 1 | board-items.test.ts "re-checks prune eligibility while holding the item lock" pins a weaker property than its name claims: the in-process fixture cannot distinguish "re-read under lock" from "read early, delete under lock". |
Suggestion, non-blocking | stands — re-measured fresh. M5 survives the core board suites 5/5 (23/23 each round); the cross-process H5 race kills it 3/3 (DELETED, pruneMs 1740–1819 ms proving lock contention) while the shipped build 3/3 PRESERVED and a pristine scratch-head control 3/3 PRESERVED; positive control M5b turns board-items.test.ts:160 red with the behavioral message expected [ Array(1) ] to deeply equal [] (same file as the mutant detector) plus board-tasks.test.ts:201. The fixture that would pin it ships in this artifact (harness/h5-race.mjs + h5-holder.mjs). No shipped-code change needed. |
| 2 | (new this round) unbounded read fan-out on the listing path | Suggestion, non-blocking | new — see Findings. |
Scope
Central claim: independently started agents (separate CLI processes, declared actors) share tasks and asks through a named filesystem board, with addressee-only settlement, a stable JSON contract, and the --wait exit-code contract 0/2/3/4.
Secondary claims: (1) malformed foreign records are skipped by listings and rejected byte-preserved by targeted mutations; (2) safety surface — private 0700/0600 permissions, safe-name validation, unique ids under concurrency, prune re-reads eligibility under the item lock.
Delta since round 6: base advanced 331 files; the merge re-resolved exactly two PR integration files (packages/cli/vitest.config.ts, packages/core/package.json); no board source file was altered by the merge.
The merge itself, verified (new scope this round)
The merge is not an evil merge for the PR's substance: git diff HEAD^2..HEAD touches only two of the PR's files, and both resolutions are correct —
| file | what main brought | what the PR kept |
|---|---|---|
packages/cli/vitest.config.ts |
environment: 'jsdom' → 'node', coverage gating switched to QWEN_CI_COVERAGE |
the @qwen-code/qwen-code-core/board alias survived |
packages/core/package.json |
version 0.22.3 → 0.23.0 |
the ./board exports entry survived |
Main's jsdom → node flip was the round's real semantic-conflict risk, because the PR's new 303-line board-cli.test.ts was written against the old config and a textual merge cannot catch that. Measured: the suite passes 18/18 with environment 0ms in the vitest timing line, confirming it never needed a document. cli.test.ts also passes at 78/78 (up from 76 — main added two tests), and main's new repo-level unit-vitest-configs gate passes 27/27 over the config the PR edits.
A/B: the wiring is load-bearing (fresh base rebuild)
Base control: fresh git worktree add tmp/base-tree HEAD^1 (cca376f6), core + cli rebuilt there. Rather than re-pointing the shared root links, the base tree got a private node_modules/@qwen-code farm: 25 of 25 workspace links resolve inside the base tree, 0 into the main tree (round 6 had acp-bridge pointing at head). Realpath witnesses quoted by the harness itself: qwen-code-core → tmp/base-tree/packages/core, qwen-code → tmp/base-tree/packages/cli. The PR leaves package-lock.json untouched, so reusing the root install is a clean control; the nested per-package node_modules a worktree does not carry were replicated (62 entries, 0 internal @qwen-code among them) after confirming they hold different majors than the root (ajv 8.20.0 nested vs 6.15.0 at root). Both worktrees were removed after capture. Witness 01-ab-base-vs-head.png; logs logs/h0-base.assert.json, logs/h0-head.assert.json, logs/build-base-{core,cli}.log.
| cell | base (control) | head (PR) |
|---|---|---|
| dist markers (M1–M7) | cli commands/board.js + board/ absent; no core board.js/board-lock/board-tasks/asks; compiled config.js does not import boardCommand |
all present; config.js does import it |
board show --board ab --json |
exit 1, Unknown arguments: board, json, no board JSON, no boards/ dir |
exit 0, exactly {"board":"ab","tasks":[],"asks":[]}, stderr clean |
board task … --as api --json |
exit 1, wrote nothing | exit 0, t- UUIDv4 id, pending/null, record 0600 under 0700 dirs |
--help |
exit 0, 0 occurrences of board |
exit 0, lists board <command> |
prune / missing --board / missing --as / raise / resolve |
all exit ≠ 0, no boards/ dir created |
prune exit 0 with shape exactly {asks,tasks}; missing flags exit 1 naming the flag; undocumented raise/resolve refused |
| validity control, both arms | --version exit 0, semver |
--version exit 0, semver |
| crash guard G1–G6, both arms | no ERR_MODULE_NOT_FOUND; refusal is a genuine yargs error |
no crash; handler reached and emits board JSON |
base 31/31 + head 32/32 = 63/63 cells.
Emission delta, fully accounted for: base core dist = 1334 .js, head core dist = 1341, Δ = 7 — board.js, board-lock.js, board-tasks.js, asks.js and the three board test files. No unexplained residue.
Architectural claim, newly measured: board.ts states that "a dedicated subpath keeps the board dependency chain out of ACP startup". A static closure walk of the compiled dist confirms it — the ./board subpath closure is 13 modules, and 0 of the 4 board modules appear in the main barrel's 669-module closure.
Findings
1. New — Suggestion (non-blocking): unbounded read fan-out makes board show fail entirely under a common descriptor limit
listAsks and listBoardTasks each fan out with a single Promise.all over every file in the collection, so the degree of parallel file I/O equals a number any process sharing the board can grow without bound.
Reproduce (the defect is fd-limit dependent, so the limit is part of the repro):
bash -c 'ulimit -n 1024; node tmp/pr9402-verify-20260903-204956/harness/h3-fdpressure.mjs --n 5000 --expect-show fail'Measured, threshold bisected through the real CLI:
ulimit -n |
records | board show --json |
outcome |
|---|---|---|---|
| 524288 (this container) | 2 000 / 10 000 / 50 000 | exit 0; 1116 ms / 1466 ms / 3553 ms | all listed, no EMFILE |
| 1024 | 200 / 500 / 900 | exit 0 | healthy |
| 1024 | 1200 / 2000 / 5000 / 50000 | exit 1, 0 bytes stdout | EMFILE: too many open files |
The cliff at 1024 sits between 900 (healthy) and 1200 (EMFILE). At the failure the command emits zero bytes — not a truncated or partial listing — so every participant loses visibility of the whole board at once. Witness 03-fdpressure-ab-shipped-vs-fix.png; logs logs/h3-lowfd.assert.json, logs/h2-bisect.log, logs/h2-scale.log.
Why this is a house inconsistency, not just an oversight. The sibling module already remediated exactly this hazard, and board-lock.ts's own header cites it as the model ("the same discipline already exists in tasks.ts and mailbox.ts"):
tasks.ts:49—MAX_PARALLEL_TASK_READS = 16, with the comment "Every teammate'stask_listtriggers a full-board read; unboundedPromise.allover a large board risks EMFILE under fd pressure."tasks.ts:680— "Bounded fan-out: an unbounded Promise.all over every task file is itself the most likely source of transient read errors (EMFILE under fd pressure) on a large board."tasks.ts:695— treats a non-ENOENTread error as "an I/O failure, not evidence of corruption — skip the file this round WITHOUT quarantining".
The board listings inherit neither half: no concurrency cap, and getAsk/getBoardTask rethrow any non-ENOENT read error, so one EMFILE rejects the whole Promise.all and the entire listing fails.
Blast radius, enumerated (not just the call site I hit): the fan-out is reachable only through snapshot() → board show, with and without --as. Every other path is single-file or sequential and is unaffected — claim/done/answer/decline (mutate/settleOnDisk, one path), ask --wait (getAsk, one path), and prune (pruneCollection, a sequential for…of under the item lock).
What it is NOT — bounded deliberately:
- Not data loss. All 5000 records survived; sha256 of a 20-record sample was unchanged after the failed read and a subsequent prune.
- Not silent. Exit 1 with
EMFILEnamed on stderr — a loud failure, which is the better of the two variants. - Not a regression. Base has no board at all, so this is new surface, not a broken baseline.
- Recoverable.
pruneexits 0 under the same fd pressure at n=5000 (measured), so a board can be pruned back below the cliff rather than being permanently unreadable. - Not a ReDoS/backtracking surface. All four anchored regexes in the board modules are fixed-length or bounded (
{0,63},{8},{4}); asserted, no nested quantifier pair.
Suggested fix — measured, not eyeballed. Bounded fan-out in batches of 16, modeled directly on tasks.ts (+34/−12 across asks.ts and board-tasks.ts; the patch is in logs/, applied and built in a scratch worktree whose only difference from head was this change — the M5 marker was confirmed absent from that build):
| check | result |
|---|---|
hostile fixture, ulimit -n 1024, n=5000 |
exit 0, 1,114,127 bytes, all 5000 listed, no EMFILE — 8/8 |
hostile fixture, ulimit -n 1024, n=50000 |
exit 0, 11,189,127 bytes, all 50000 listed, 2662 ms — 8/8 |
| zero collateral, end to end | the full 114-cell behavioral suite passes 114/114 against the fix build, with cell names and order byte-identical to the shipped build (logs/h1-fixarm.assert.json) |
| affected suite counts unchanged | core board suites 23/23 on the fix source |
| cost | 1083 ms at n=5000 where the shipped build crashed at 1037 ms — the cap is free |
The fixture that would pin this is harness/h3-fdpressure.mjs run under bash -c 'ulimit -n 1024; …'. Note the suite is currently unpinned along this axis: it is green both with and without the patch, because no existing test lowers the descriptor limit.
Inference, labelled as such: I could not measure macOS's default soft limit (256) from this Linux container. Scaling from the 1024 cliff, a 256 limit would put the threshold proportionally lower — the PR reports macOS ✅, presumably with a small board.
2. Carried from rounds 1–6 — Suggestion (completeness, non-blocking): the lock-recheck test pins the gate, not its position
Re-measured fresh; see the status table. The distinction the fixture cannot make is now isolated cleanly by two mutants that differ in only one respect:
| mutation | what changes | cross-process race | in-process suite |
|---|---|---|---|
| M5 | eligibility read hoisted outside the lock, gate kept | DELETED 3/3 — a record reopened while the lock was held is destroyed | SURVIVED 5/5 (23/23) |
| M5b (positive control) | gate removed, read kept under the lock | n/a | KILLED — board-items.test.ts:160, expected [ Array(1) ] to deeply equal [] |
So the suite does pin the gate (M5b dies at the intended assertion, in the same file as the mutant's detector, with an expected-vs-actual behavioral message rather than an import or compile break), but nothing pins where the read happens relative to the lock. The in-process fixture holds the mutex in the same process and rewrites after a setImmediate, so no ordering forces the read to precede the rewrite — a read-early implementation still sees the freshened bytes. The shipped code is correct; this is a test to add, not code to change. Witness 04-prune-race-m5-matrix.png; logs logs/h5-*.assert.json, logs/m5-inprocess-round{1..5}.log, logs/m5b-inprocess.log, logs/finding1-driver.log.
3. Nice to have: prune requires --as and then discards it
packages/cli/src/commands/board.ts calls requireActorName(a.as); as a bare statement in the prune handler — the validated name is never used. Every other mutating command records its actor in the record it writes; prune deletes records and leaves no trace of who pruned. The user doc says "--as is a label recorded with the action", which holds for task/claim/done/answer/decline but not for prune. Measured: board prune --as human succeeded with no actor recorded anywhere on disk. Either record the actor or drop the requirement; the uniform-flag argument for keeping it is reasonable, but the doc sentence is the part that overstates it.
4. Note, not a defect: two refusal paths for unsafe names
All 11 unsafe board names and all 4 unsafe actor names are refused (exit ≠ 0) with no stray directory created — but by two different mechanisms, now pinned separately: 10 board names and 3 actor names are rejected by assertSafeName, while a leading-dash value (-lead, -x) is consumed by yargs as flags and never reaches the guard (exit 1 with usage text). Consequently the "must start with a letter or digit" clause of SAFE_NAME is unreachable from the CLI for dash-leading names; it remains reachable programmatically and is what catches .., trail., device names, and 65-character names. Outcome is correct either way.
Corrections
A correction to the verification method, not to the PR — and one that bears on how round 6's base arm should be read. A fresh git worktree does not carry the gitignored packages/cli/src/generated/git-commit.ts, which commands/review/lib/sandboxed-exec.js imports. Without it, every board invocation in a scratch-built CLI dies with ERR_MODULE_NOT_FOUND and exit 1 — which also satisfies an assertion of the form "base refuses this, exit ≠ 0". I hit exactly this: my first base arm passed 25/25 with the refusals coming from a crashing build rather than from yargs. Fixed by running node scripts/generate-git-commit-info.js in the scratch trees (base → cca376f, head → f1d7dc8), rebuilding, and adding crash-guard cells G1–G6 that assert the refusal reason (Unknown arguments) and that stderr contains no module/startup crash. The corrected base arm is 31/31.
Round 6's report describes the same fresh-worktree base rebuild and does not mention the generated file, so its base-arm behavioral cells may have passed for that wrong reason. The A/B conclusion is unaffected, because it does not rest on those cells: the dist-marker cells (M1–M7) and the --help census are immune to the crash, and they independently establish that base contains no board code. Recorded here so the next round does not inherit the trap.
Two further harness bugs were found and fixed before counting, neither PR behavior: the first h5-holder.mjs used fs.readFileSync on the node:fs/promises namespace, so the holder crashed at its rewrite (~1.2 s) and released the lock early — the race silently degenerated into "prune deletes an expired record", and the shipped build appeared to fail; the holder is now asserted to exit 0 in every round, and its timeline (acquired/rewrote/released) is printed so a non-race is visible. And three initial H1 cells encoded wrong expectations (a --json waiter asserted against human output; unsafe names asserted against one message class when yargs supplies another); all three were corrected to assert the real contract, and the corrected forms are stricter than the originals.
Not covered
- Windows runtime behavior — Linux-only verification; the PR itself marks Windows smoke as pending.
WINDOWS_DEVICE_NAMErejection was exercised only as a string check on Linux. - macOS 256-descriptor threshold — inferred from the 1024 cliff, not measured (see Finding 1).
- Per-commit attribution — depth-2 checkout reaches only the merge commit,
HEAD^1, andHEAD^2.git rev-list HEAD^1..HEAD^2returns 1 against 55 commits in the metadata snapshot: the known shallow-boundary artifact, not a real count.git merge-base HEAD^1 HEAD^2returns nothing. Verified the aggregateHEAD^1..HEADdiff only. - Base ref note — the snapshot's
baseRefOid(19182d08…) differs from the local merge-ref baseHEAD^1(cca376f6…); per the merge-ref contract the local checkout is authoritative and was used throughout. - Repo-wide test suite and lint — gates were scoped to the affected surface plus the two repo-level config gates main added this round.
scripts/tests/package-scripts.test.tsdid not match the config's include pattern under the invocation I used and was not run. - Bundle entry — harnesses drove
packages/cli/dist/index.js(tsc output), notdist/cli.js. docs/plans/2026-08-18-peer-session-collaboration.md(213 lines) — not audited against the code. The user-facingdocs/users/features/agent-board.mdwas audited: every checkable claim matched measurement, including the exit-code table 0/2/3/4,--timeout/--ttlin seconds,--older-thanin days,--asfiltering semantics forshow, the~/.qwen/boards/layout, and "pruning rechecks each record while holding its lock" — the last being exactly the property Finding 2's race proves and M5 breaks.- Two diagnostic probe runs are deliberately excluded from
assertions.json. The initial low-fdh2-scaleruns (rungs 5000/50000, then the 200–2000 bisect) were written with expectations encoded for a healthy arm, so they reported failures while discovering the defect. They are superseded byh3-fdpressure.mjs, which encodes the demonstrated defect as the expected outcome and passes 8/8. Counting the mis-encoded discovery runs would have stamped ❌ on the PR for a harness expectation, so only the correctly-encoded runs are counted; both sets of raw output are inlogs/. - Flakiness gate on changed test files is run by the workflow, not this round.
Methodology
One container (node:22-bookworm, shared loaded runner, Node v22.23.2, ulimit -n 524288 soft and hard), working tree at refs/pull/9402/merge, git status clean before and after. The PR's CLI ran as real child processes with a unique QWEN_HOME per harness section and FORCE_COLOR/NO_COLOR stripped from child env so stderr-quiet cells stay meaningful; --wait cells used real background waiter processes settled from separate invocations, and the fd-pressure cells ran under bash -c 'ulimit -n 1024; …'. The base control and both mutation arms were scratch git worktrees under tmp/, each provisioned by harness/setup-tree.mjs with a private 25-link @qwen-code farm (0 links into the main tree) and 62 replicated nested node_modules entries; realpath witnesses are printed by the harness rather than asserted from config. The prune race used a real second OS process (harness/h5-holder.mjs) taking the same proper-lockfile lock withItemLock uses, rewriting at +1.2 s inside a 2.0 s hold while prune provably blocked on retries (pruneMs 1736–1836 ms). The candidate fix was built in a scratch arm differing from head by only that change and driven through the same harnesses via a VERIFY_CLI override. Every gate cited is proven live by a planted defect with the restore sha256-verified, and the two tsc gates were additionally planted in the main tree to rule out an incremental-buildinfo false green (there is no tsconfig.tsbuildinfo at the core package root, so the clean --noEmit was a full check; the plant produced asks.ts(148,9): error TS2322, exit 2). Scratch-build type errors were A/A-controlled by building base and head the identical way and observing the same single unrelated TS7016. Assertion counts are re-derived from the saved artifacts by harness/tally.mjs, never retyped: 63 (H0) + 228 (H1 × 2 arms) + 11 (H2) + 24 (H3 × 3) + 45 (H5 × 3) + 14 (liveness) + 20 (gate, mutation, A/A and build-integrity checks re-parsed from logs) = 405. Evidence images produced by scripts/verify-capture.mjs; raw per-cell logs in logs/.
Flakiness gate log
rounds=5 files=5 skipped=0
file packages/cli/src/cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/cli.test.ts
file packages/cli/src/commands/board/board-cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/board/board-cli.test.ts
file packages/core/src/agents/team/asks.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/asks.test.ts
file packages/core/src/agents/team/board-items.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-items.test.ts
file packages/core/src/agents/team/board-tasks.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-tasks.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/cli/src/cli.test.ts: PPPPP
packages/cli/src/commands/board/board-cli.test.ts: PPPPP
packages/core/src/agents/team/asks.test.ts: PPPPP
packages/core/src/agents/team/board-items.test.ts: PPPPP
packages/core/src/agents/team/board-tasks.test.ts: PPPPP
verdict: pass
summary: 5 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/cli.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 2 · packages/cli/src/cli.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 3 · packages/cli/src/cli.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 4 · packages/cli/src/cli.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 5 · packages/cli/src/cli.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
Local runtime validation — real build, real processesI built PR head Verdict: the Board layer behaves exactly as documented. Recommend merge, with the follow-ups below. Build: link-farmed worktree, 1. Two independently started agents actually share workNo team, no daemon, no shared process — two plain shells, one board. Task → claim → ask → answer → done, with
2. Concurrency holds up
I proved the exclusivity guard is load-bearing by deleting it from the shipped bundle and re-running the same race: 3. Test-plan step 4 (prune vs. a reopened record) genuinely worksI wrote a foreign-runtime process that takes the item lock, holds it while 4. Storage hardening (test-plan steps 3 and 5)Path/device-name rejection, malformed foreign records skipped without being rewritten, FindingsF1 ·
|
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 206 passed · 0 failed · 206 total Flakiness gate: ✅ 5 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:206 通过 · 0 失败 · 206 总计 抖动门:✅ 5 changed test file(s) x 5 identical rounds, no divergence Verification report<!-- qwen-triage:verify --> Sandboxed verification: ❌ not passed — findings reported (agent verdict) Ran the PR in an isolated, token-free container: live reconfirmation harnesses against the built head CLI, targeted gates with liveness plants, and a zero-delta input-closure proof against the previous round. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 206 passed · 0 failed · 206 total 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)本轮为第八次跟进验证,且是零增量轮:merge/base/head/tree 四个 OID 与第七轮逐字节相同(
Verification reportPR #9402 Deep Verification (round 8) — feat: agent boardVerdict: Verified head Previous-round findings at the new head
Zero-delta: what was compared, and what was carried vs re-runRound 7 recorded merge Carried under that identity (not re-run): the base-rebuild A/B wiring matrix (63 cells: base 31 + head 32, dist markers M1–M7, Re-measured live this round (206 assertions): keystone probe H1 (162), fd cliff H2 (17), gates + liveness plants (20), zero-delta proof (7). ScopeCentral claim: independently started agents (separate CLI processes, declared actors) share tasks and asks through a named filesystem board, with addressee-only settlement, a stable JSON contract, and the Live reconfirmation at this headWitnesses:
Reviewer Test Plan, per step
Findings1. Carried from round 7 (new there) — Suggestion, non-blocking: unbounded read fan-out makes
|
ulimit -n |
records | board show --json |
outcome |
|---|---|---|---|
| 1024 | 900 | exit 0, 900 listed | healthy |
| 1024 | 1200 | exit 1, 0 bytes stdout, single-line EMFILE stderr |
whole-board visibility loss |
| 1024 | 1200, then prune |
prune exit 0, removes 1200; show exit 0 | recoverable |
| 524288 | 2000 | exit 0, 2000 listed | healthy (limit-dependent) |
Boundaries re-stated from round 7 and still true of this identical tree: not data loss (sha256 sample unchanged), not silent (loud exit 1), not a regression (base has no board), recoverable via prune; sibling tasks.ts already carries MAX_PARALLEL_TASK_READS = 16 with the exact rationale comment; the measured candidate fix (+34/−12, batches of 16) passed 8/8 hostile cells at n=5000/50000, 114/114 zero-collateral, 23/23 suite counts — carried, not rebuilt. The suite remains unpinned along this axis (green with and without the patch).
2. Carried from rounds 1–6 — Suggestion (completeness, non-blocking): the lock-recheck test pins the gate, not its position
Status table row 2. No shipped-code change needed; the fixture that would pin it ships in round 7's artifact (h5-race.mjs + h5-holder.mjs).
3. Carried — Nice to have: prune requires --as and then discards it
packages/cli/src/commands/board.ts calls requireActorName(a.as); as a bare statement in the prune handler. Re-measured live (K5.3): the actor name appears nowhere on disk after a prune, while the user doc says "--as is a label recorded with the action". Either record it or drop the requirement; the doc sentence is the part that overstates.
4. Carried — Note, not a defect: two refusal paths for unsafe names
Re-measured live (K6.1/K6.2). Outcome correct either way; the "must start with a letter or digit" clause of SAFE_NAME is unreachable from the CLI for dash-leading values (yargs consumes them), and remains the guard for .., trail., device names, and 65-character names.
Completeness reporting, not a PR defect: the vitest alias is redundant defence for the board-cli suite
The mutation/liveness matrix this round includes one adjudicated survivor. Removing the @qwen-code/qwen-code-core/board alias from packages/cli/vitest.config.ts (plant LP2) leaves G2 green at 18/18, for two independent reasons visible in the code: board-cli.test.ts wraps the specifier in vi.mock('@qwen-code/qwen-code-core/board', …), so its assertions never touch the resolved module; and the PR's own "./board" exports entry in packages/core/package.json resolves the specifier to the built dist/src/board.js whenever the alias is absent. Classification: redundant defence, not dead code — the alias still decides whether cli tests exercise core source or core dist (protection against a stale or missing dist, and the same house pattern as the other core subpath aliases in that file), it is simply not load-bearing for this suite while dist is built. G2's positive control is plant LP2b in the same suite's exercised source: flipping the declined --wait exit code 2→5 in board.ts turns G2 red at 1 failed | 17 passed with the pinned detector maps an ask declined outcome to exit code 2 and expected-vs-actual values.
| plant | mutation | result | detector |
|---|---|---|---|
| LP1 | asks.ts: current.to !== by → === by |
G1 red 3 failed | 20 passed |
× board asks > lets only the addressed actor answer or decline, message addressed to "web" |
| LP2 | remove the board alias from cli vitest config | G2 green 18/18 (survivor, classified above) | n/a |
| LP2b | board.ts: declined exit code 2 → 5 |
G2 red 1 failed | 17 passed |
maps an ask declined outcome to exit code 2, expected-vs-actual |
| LP3 | asks.ts: append const qvPlant: number = 'x'; |
G5 red | src/agents/team/asks.ts(298,7): error TS2322 |
Every plant was restored and the restore sha256-verified against the pre-plant hash (logs/pre-plant-shas.txt, sha256sum -c OK), the post-plant re-runs are green (23/18/tsc-clean), and git status --porcelain shows no tracked modification afterwards.
Corrections
Carried from round 7 (to the verification method, not the PR): a fresh git worktree does not carry the gitignored packages/cli/src/generated/git-commit.ts; without it every scratch-built board invocation dies with ERR_MODULE_NOT_FOUND, which also satisfies "base refuses this, exit ≠ 0". Any future base rebuild must run node scripts/generate-git-commit-info.js first and assert the refusal reason, not just the exit code. Not re-exercised this round (no base rebuild).
New this round (to this round's own method, not the PR): three harness defects were found and fixed before counting, none of them PR behavior. (a) The recorder ended with process.exit(), which truncates pending async stdout writes when stdout is a pipe — the first H1/H2 piped runs lost their console reports (assert JSONs, written before the exit, were intact); fixed to process.exitCode, and the counted runs are the direct file-redirect ones (POSIX file stdout is synchronous). (b) scripts/verify-capture.mjs's pipe mode reads stdin with readFileSync(0), which throws against a producer that has not emitted yet — captures therefore use the -- cat <log> form. (c) The first plants run wrote every G1/G2 invocation to one log name, so post-plant green runs overwrote the red logs; naming was made per-run and the plants re-executed. Separately, the initial LP2 encoding predicted red where the demonstrated outcome is green (the survivor above); that discovery run is archived and excluded from the tally, exactly as round 7 handled its mis-encoded discovery runs.
Not covered
- Base-rebuild A/B, M5/M5b race matrix, candidate-fix arm — carried under the proven-identical input closure (comparison table above), not rebuilt this round. Test Plan step 4's two-process race is part of this carry.
- Full 114-cell behavioral suite — superseded this round by the 162-assertion keystone subset covering the same contracts end to end; round 7's per-cell numbers carry.
- Windows runtime — Linux-only; the PR itself marks Windows smoke pending.
- macOS 256-descriptor threshold — inferred from the 1024 cliff in round 7, not measured.
- Per-commit attribution — depth-2 checkout reaches only the merge commit,
HEAD^1,HEAD^2;git rev-list HEAD^1..HEAD^2returns 1 against 55 commits in the metadata snapshot (the shallow-boundary artifact). AggregateHEAD^1..HEADdiff verified only. - Base ref note — the snapshot's
baseRefOid(19182d08…) differs from the local merge-ref baseHEAD^1(cca376f6…); per the merge-ref contract the local checkout is authoritative. - Repo-wide test suite and lint, bundle entry (
dist/cli.js), flakiness gate — gates scoped to the affected surface; the flakiness gate is workflow-owned. docs/plans/2026-08-18-peer-session-collaboration.md— not audited (carried). The user-facingdocs/users/features/agent-board.mdaudit carries from round 7; its checkable claims were spot-covered again by H1 (exit-code table,--ttl/--older-thanunits,--asfilter semantics,~/.qwen/boards/layout).
Methodology
One container (node:22-bookworm, shared loaded runner, Node v22.23.2, ulimit -n 524288 soft and hard), working tree at refs/pull/9402/merge, git status clean apart from the artifact dir before and after. H1/H2 drove the real built CLI (packages/cli/dist/index.js) as child processes with a unique QWEN_HOME per section and FORCE_COLOR stripped; --wait cells used real background waiter processes settled from separate invocations; fd-pressure cells ran under bash -c 'ulimit -n 1024; …'; records for the fd ladder were seeded directly as schema-valid aged records so the probe measures the read path. Gates ran the affected suites and both package typechecks from their package dirs; every gate's liveness is proven by a planted defect (LP1/LP2b/LP3) whose red run names the intended detector with expected-vs-actual values, with sha256-verified restores and green post-plant re-runs. Assertion counts are re-derived from the saved assert JSONs by harness/tally.mjs, never retyped: 162 (H1) + 17 (H2) + 4 (G1–G4) + 2 (G5/G6) + 10 (plants-suite) + 4 (plants-tsc) + 7 (zero-delta) = 206. Evidence images produced by scripts/verify-capture.mjs in its -- cat <log> form; raw per-cell logs in logs/.
Evidence images
— Qwen Code · sandboxed verification
Flakiness gate log
rounds=5 files=5 skipped=0
file packages/cli/src/cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/cli.test.ts
file packages/cli/src/commands/board/board-cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/board/board-cli.test.ts
file packages/core/src/agents/team/asks.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/asks.test.ts
file packages/core/src/agents/team/board-items.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-items.test.ts
file packages/core/src/agents/team/board-tasks.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-tasks.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/cli/src/cli.test.ts: PPPPP
packages/cli/src/commands/board/board-cli.test.ts: PPPPP
packages/core/src/agents/team/asks.test.ts: PPPPP
packages/core/src/agents/team/board-items.test.ts: PPPPP
packages/core/src/agents/team/board-tasks.test.ts: PPPPP
verdict: pass
summary: 5 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/cli.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 2 · packages/cli/src/cli.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 3 · packages/cli/src/cli.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 4 · packages/cli/src/cli.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 5 · packages/cli/src/cli.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
qqqys
left a comment
There was a problem hiding this comment.
COMMENT — 唯一站立阻塞项:R10-1(qwen board --json 输出绕过终端消毒)在精确 head 机制原样在场
Re-reviewed at head 7e998c25717c99f66293f314f3b68ac85487c657。76cccf2a..head 增量全为合入 main(100 个 main PR),无任何触碰 packages/cli/src/commands/board* 或 packages/core/src/agents/team/board* 的提交——该区间内的评审记录与代码事实在本 head 原样成立。
站立的阻塞项(逐锚核实)
R10-1 [Critical] packages/cli/src/commands/board.ts emit()(head 上 :38): --json 分支写 JSON.stringify(value) 未经 sanitizeTerminalText,而相邻 human 分支显式消毒——JSON.stringify 只转义 U+0000–U+001F、引号与反斜杠,不处理 bidi override/isolate(U+202A–U+202E、U+2066–U+2069、U+200E/F)与 C1/DEL。board 在自身 plan 文档里即被声明为不受信的跨 agent 通道,assertText 仅查空与 64KB 上限;八个子命令的 --json 全走此分支。受害者跑 qwen board show --json 或管道给 jq(原样渲染 Unicode)时,恶意 peer 写入的 U+202E 可视觉重排显示文本(CVE-2021-42573/42656 类)。round-13 复审(2026-09-01 19:25)将其列为 Unresolved, please confirm — the mechanism is present at HEAD,其后无任何 board 提交;wenshao 的批准与 --json = 机器消费,消毒破坏数据保真 的立场都是裁量理由而非机制修复。
可操作修法(二选一):
- 在
--json分支对JSON.stringify(value)的结果做「仅替换 bidi/C1 为转义形式」的保真消毒(如把 U+202A-U+202E、U+2066-U+2069、U+200E/F、DEL、C1 重写为\uXXXX——仍是合法 JSON,jq/parse 还原后字节不变),并加一个含 U+202E 的端到端 CLI 见证(移除映射即红);或 - 由维护者在 R10-1 线程上署名接受
--json面向机器消费者的边界(resolve 线程),把「人肉看 JSON 输出」声明为不支持面。
其余门禁
- round-13 的 8 条重复 Suggestion 与 deferred 清单按规则不卡点。
- CI(非门禁,仅陈述):本 head 19 success / 27 路径性 skip / 0 失败(早前
Test (ubuntu)的红随 main 合入已消除)。 - 代码面(除 R10-1 外)抽查:lockfile 协作、claim/TTL 有限校验、stdout 排空写、EPIPE 处理与 exit 时序注释自洽。
结论: 提交 COMMENT。作者落修法 1 或取得维护者裁断(修法 2)后即可在同 head 批准;二者都不需要重开新评审。
— 衍星 · read-only PR review (posted as qqqys)
|
Triage re-run completed without a new review.
The stage comments above were updated with the latest result. View workflow run. 上方各阶段评论已更新为最新结果。查看工作流运行。 |
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 191 passed · 0 failed · 191 total Flakiness gate: ✅ 5 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:191 通过 · 0 失败 · 191 总计 抖动门:✅ 5 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #9402 Deep Verification (round 9) — feat: agent boardVerdict: Verified head 中文 — 判定:`findings`(报告了发现)本轮为第九次跟进验证,且不是零增量轮:PR head 未变(仍为
Previous-round findings at the new headRound 8 was a zero-delta round and carried finding #2 without re-running it. That carry is void this round: the tree OID changed, so every input closure changed with it. All four findings were re-measured live.
What changed since round 8, and why nothing was carried
Whole-repo tree-OID identity was round 8's basis for carrying expensive measurements. It does not hold now, so nothing was carried: the base-rebuild A/B wiring matrix, the 114-cell behavioral suite, the fd ladder, the M5/M5b race matrix, the candidate-fix arm, and the alias survivor were all either re-run live this round or are listed under Not covered. Round 8's base ScopeCentral claim: independently started agents — separate CLI processes, declared actors — share tasks and asks through a named filesystem board, with addressee-only ask settlement, a stable JSON contract, and the Structural note on the A/B shape. The effective diff Central claim: verified at the new baseWitness The highest-value round-9 probe is K0, which round 8 could not have made in the same form: the new base ships runtime-scoped storage (
Reviewer Test Plan, per step
Gates and livenessEvery gate was re-run on the merged tree, and every cited gate was proven able to fail by a planted defect in the file it exercises. Witness
Every plant was restored and sha256-verified against its pre-plant hash ( LP2 survivor classification, re-measured and unchanged: redundant defence, not dead code. Base-interaction probe: the subpath isolation claim holds
The 13-module subpath closure is Boundary observation, not a defect. Because the board root deliberately ignores Findings1. Carried from round 7, re-measured and sharpened — Suggestion, non-blocking: unbounded read fan-out makes
|
ulimit -n |
records | board show --json |
outcome |
|---|---|---|---|
| 1024 | 900 | exit 0, 900 listed, 190725 B | healthy |
| 1024 | 1000 | exit 0, 1000 listed, 211925 B | last healthy (bisected) |
| 1024 | 1010 | exit 1, 0 B stdout, EMFILE |
first failing (bisected) |
| 1024 | 1200 | exit 1, 0 B stdout, single-line EMFILE |
whole-board visibility loss |
| 1024 | 1200 asks | exit 1, 0 B stdout | identical cliff on the asks collection |
| 1024 | 1200, then prune |
prune exit 0 removes 1200; show exit 0 | recoverable |
| 524288 | 2000 | exit 0, 2000 listed | healthy (limit-dependent) |
Boundaries re-measured and still true: not data loss (sampled sha256s unchanged), not silent (loud exit 1 with EMFILE named), not a regression (base has no board), recoverable via prune, which reads serially under the item lock and therefore works at the same limit that breaks show.
The candidate fix is measured at this base, not eyeballed (H6 13/13). Batching the reads in groups of 16 — mirroring the pattern the base's own agents/team/tasks.ts already ships as MAX_PARALLEL_TASK_READS = 16 at line 683 — was applied to the compiled modules and driven through the same probes:
| cell | unpatched | patched |
|---|---|---|
1200 records @ ulimit -n 1024 |
exit 1, 0 B stdout, EMFILE |
exit 0, 246125 B, 1200 listed, stderr clean |
5000 records @ ulimit -n 1024 |
— | exit 0, 5000 listed — the bound removes the size-dependence entirely |
900 records @ ulimit -n 1024 |
exit 0 | exit 0, 900 listed (no regression) |
| 200 records, same bytes on disk | 40925 B | 40925 B, byte-identical output (zero collateral: order, content, shape unchanged) |
Both compiled modules were restored and sha256-verified. The suite remains unpinned along this axis: no test distinguishes the bounded from the unbounded read, so a fix should ship with a fixture that seeds past the batch boundary under a lowered descriptor limit.
2. Carried, re-measured live — Suggestion (completeness, non-blocking): the lock-recheck test pins the gate, not its position
Status table row 2; H5 evidence above. No shipped-code change is needed — the behaviour is correct. The gap is that board-items.test.ts cannot distinguish "re-read under the lock" from "read early, delete under lock", because in-process the prune's own read is delayed by the mutex the test already holds. The fixture that does pin it is harness/h5-race.mjs + harness/h5-holder.mjs in this artifact: two real processes, the reopen placed after the pruner's read and before the lock release.
3. Carried, re-measured — Nice to have: prune requires --as and then discards it
packages/cli/src/commands/board.ts calls requireActorName(a.as); as a bare statement in the prune handler. H1 K5.7: the actor name appears in 0 files under the boards root after a prune, while docs/users/features/agent-board.md says "--as is a label recorded with the action". Either record it or drop the requirement; the doc sentence is the part that overstates.
4. Carried, re-measured and widened — Note, not a defect: two refusal paths for unsafe names
H1 K6.1–K6.5. Outcome correct either way. The "must start with a letter or digit" clause of SAFE_NAME is unreachable from the CLI for dash-leading values (yargs consumes them as flags), and remains the guard for .., ., trail., Windows device names, 65-character names, spaces and slashes — 12/12 of which were refused this round with no stray directory created.
Corrections
(a) To round 8's report, not to the PR — the completion note is JSON-only. Round 8's Test Plan step 1 row reported "owner web, status completed, note all good in both JSON and human output". Measured this round (K1.12): the human show output for that board is exactly board: k1\n- t-9f77b28a-… [completed] web: check the API — renderBoard in packages/cli/src/commands/board/render.ts emits - {id} [{status}] {owner}: {subject} and has no note field, so all good does not appear. The PR's own Test Plan asks only for show --board demo --json to report the note, which it does (K1.9), so this is a correction to the previous verification round's description, not a request to change the code. Worth knowing because it means the completion note is invisible without --json.
(b) To round 8's report — the LP3 detector code. Round 8 cited error TS2322 at asks.ts(298,7). This round the same plant reports TS6133: 'qvPlant' is declared but its value is never read at that position, with TS2322 also present in the output. Immaterial to the conclusion (the typecheck gate is live); recorded so the two rounds do not appear to disagree.
(c) To round 8's Finding 1 bracket. The cliff was reported as "between 900 (healthy) and 1200". Bisected through the real CLI this round to 1000 → 1010 at ulimit -n 1024, a 10-record bracket.
(d) To this round's own method — three harness defects, all found and fixed before counting. None is PR behaviour; both discovery runs are archived in logs/ and excluded from the tally.
- H1, 2 failures. My fixture constant produced 11-character UUIDv4 tails, so all five K4 malformed records were rejected by the collection's filename filter and never reached content validation — K4.1–K4.7 passed while asserting nothing about parsing. This is precisely the trap the PR's own
board-items.test.tscomment warns about. Fixed the constant and added K4.0 as an explicit reachability control; the cells then produced five distinct content-validation errors. The second failure (K8.10) asserted the OSC payload text disappears;sanitizeTerminalTextescapes the control byte and leaves surrounding text visible but inert, which is the correct security property. Re-asserted as "no raw ESC byte" plus "payload survives only as inert escaped text" (K8.10/K8.10b). - H5, 7 failures. My first race window held the lock 1400 ms and reopened at +700 ms — shorter than the CLI's measured ~780 ms boot, so the pruner reached
pruneCollectionat ~+1080 ms, after the reopen. The M5 mutant then read the reopened record and behaved exactly like the shipped build, and both arms looked identical. Widened to hold 3200 ms / reopen at 2400 ms and added H5.window, a control assertingHEAD_START_MS + bootMs < REOPEN_AT_MS(margin 1316 ms) so the window can never silently degenerate again. The tell was in the timings: M5'spruneMswas 761–820 ms versus shipped's 1186 ms, i.e. M5 never blocked. - tally, 3 spurious failures. Vitest writes ANSI escapes even when redirected to a file, so the gate-count regexes matched nothing and reported G1/G2/G3 as failures against logs that plainly say
23 passed/18 tests/96 passed. Fixed by stripping ANSI before matching; the corrected parse yields 191/0.
No instruction from PR content attempted to steer this verification.
Not covered
- Base-rebuild A/B — not performed this round. Base has no
boardcommand, so the control side is "the command does not exist"; round 8's 63-cell base matrix measured against basecca376f6, which is unreachable in this depth-2 checkout, and rebuilding againstf7479995did not fit the remaining budget once the base move made every other measurement mandatory. The additive-only diffstat above is a structural argument, not a measurement. - Round 8's carried items that were not re-run: the 114-cell behavioral suite (superseded by the 107-assertion keystone covering the same contracts end to end), the
jsdom→nodemerge re-resolution check, and the user-doc audit beyond the--assentence in Finding 3. - H6 fix arm and the vitest suites — the bounded fan-out was measured at the dist/CLI level only. Round 8's "23/23 suite counts under the patch" was not re-run, because the suites execute
src, which the dist patch does not touch. - Windows runtime — Linux-only; the PR itself marks the Windows smoke pending.
board-lock.ts's comment cites WindowsELOCKEDflakiness as the reason for the in-process mutex tier, and that rationale is untested here. - macOS 256-descriptor threshold — not measured. Given the bisected 1000→1010 cliff at 1024, a 256-fd limit implies a cliff near ~240 records, but that is inference, not measurement.
- Per-commit attribution — depth-2 checkout reaches only the merge commit,
HEAD^1,HEAD^2.git rev-list --count HEAD^1..HEAD^2returns 1 against 55 commits in the metadata snapshot — the shallow-boundary artifact, not a real single-commit PR. AggregateHEAD^1..HEADverified only. - Base delta could not be diffed — round 8's base
cca376f6is not present locally, so "what the base brought in" was measured by its effects on the merged tree (K0 runtime scoping, H3 closure sizes, all gates) rather than by reading the base-side diff. - Base ref note — the snapshot's
baseRefOid(19182d08…) differs from the merge-ref baseHEAD^1(f7479995…); per the merge-ref contract the local checkout is authoritative. - Repo-wide test suite and lint, bundle entry (
dist/cli.js), flakiness gate — gates scoped to the affected surface; the flakiness gate is workflow-owned. docs/plans/2026-08-18-peer-session-collaboration.md(213 lines) — not audited.docs/users/features/agent-board.mdwas spot-checked only where a finding cites it (the--assentence); its exit-code table and--ttl/--older-thanunits are covered indirectly by K3/K5.
Methodology
One container (node:22-bookworm, shared loaded runner, Debian 12, Node v22.23.2, ulimit -n 524288 soft), working tree at refs/pull/9402/merge, git status clean apart from the artifact dir before and after (asserted). H1/H2/H5/H6 drove the real built CLI packages/cli/dist/index.js as child processes — no mock or stub of the unit under test — with a unique QWEN_HOME per section, FORCE_COLOR stripped, and --wait cells using real background waiter processes settled from separate invocations. H2/H6 ran under bash -c 'ulimit -n 1024; …' and measured stdout as a byte count from a redirected file, not as a string, so "0 bytes" is literal. H5 drove the cross-process lock with a holder process importing the compiled withItemLock from dist, and its M5 arm patched the compiled pruneCollection in place — read early, decide early, delete under lock — restoring and sha256-verifying afterwards. H3 walked the built ESM import graph statically from dist/src/index.js, dist/src/board.js, dist/src/agents/team/index.js and cli's dist/src/cli.js. H6 patched the two compiled list functions to batch reads in 16s, comparing patched against unpatched output on the same seeded bytes so the zero-collateral claim is byte identity rather than a shape match. Gates ran the affected suites and both package typechecks from their package dirs; every gate's liveness is proven by a planted defect whose red run names the intended detector with expected-vs-actual values, with sha256-verified restores and green post-plant re-runs. Assertion counts are re-derived from the saved assert JSONs and ANSI-stripped gate logs by harness/tally.mjs, never retyped: 107 (H1) + 15 (H2) + 7 (H3) + 27 (H5) + 13 (H6) + 17 (plants) + 5 (gates) = 191. Two harness-defect discovery runs are archived as logs/h1-keystone-discovery-run.log and logs/h5-race-discovery-run.log and excluded. Evidence images produced by scripts/verify-capture.mjs in its -- cat <log> form.
Flakiness gate log
rounds=5 files=5 skipped=0
file packages/cli/src/cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/cli.test.ts
file packages/cli/src/commands/board/board-cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/board/board-cli.test.ts
file packages/core/src/agents/team/asks.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/asks.test.ts
file packages/core/src/agents/team/board-items.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-items.test.ts
file packages/core/src/agents/team/board-tasks.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-tasks.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/cli/src/cli.test.ts: PPPPP
packages/cli/src/commands/board/board-cli.test.ts: PPPPP
packages/core/src/agents/team/asks.test.ts: PPPPP
packages/core/src/agents/team/board-items.test.ts: PPPPP
packages/core/src/agents/team/board-tasks.test.ts: PPPPP
verdict: pass
summary: 5 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/cli.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 2 · packages/cli/src/cli.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 3 · packages/cli/src/cli.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 4 · packages/cli/src/cli.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 5 · packages/cli/src/cli.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/asks.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
Resolve the three paths both sides touched: - packages/cli/src/config/config.ts and packages/core/package.json merge cleanly: main's changes and this branch's boardCommand registration / "./board" export do not overlap. - packages/cli/vitest.config.ts keeps main's version. This branch only added an explicit '@qwen-code/qwen-code-core/board' alias, and main has since replaced the object-form alias map with an ordered array whose /^@qwen-code\/qwen-code-core\/(.*)$/ wildcard already maps that specifier to <core>/src/board, i.e. the board.ts this branch adds. Main's named list is documented as holding only subpaths whose target cannot be derived from the specifier, and board can be, so the explicit entry is now redundant rather than dropped behaviour. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-conflict/jmtpmlcodmz





































What this PR does
Adds a filesystem-backed board through which independently started agents can share tasks and questions. Every command names the board explicitly with
--board; every mutation declares its actor with--as. There is no membership, join/leave flow, participant record, heartbeat, roster, ambient identity, process-global board context, launcher, wake path, or runtime addressing.The storage layer uses private directories/files, UUID item ids, exclusive creation, per-item in-process and cross-process locks, validated records, atomic updates, and lock-scoped prune eligibility checks. Malformed foreign records are skipped by listings and rejected without being rewritten by targeted mutations.
The CLI supports
show,task,claim,done,ask,answer,decline, andprune, with stable JSON output. Bounded ask waits distinguish answered, declined, item-TTL timeout, and local-wait timeout outcomes by exit code.This PR also includes the design from #9399. It implements the Board storage and CLI contract. A standalone merge is a maintainer decision to ship that low-level experimental surface; otherwise it should land with a concrete native consumer or runner.
Why it's needed
Agent Team coordinates Qwen workers spawned by one session, while #8724 owns discovery and messaging between already-running Qwen Code sessions. Neither gives a non-Qwen process a durable shared work surface. A pull-based CLI is the smallest storage contract shared by Qwen Code, Codex, shell scripts, and scheduled jobs, but it is not itself the user-facing scheduler.
Reviewer Test Plan
How to verify
qwen board task "check the API" --board demo --as api --json; claim and complete the returned id as another actor. Expectshow --board demo --jsonto report the completed task, owner, and note.apitoweb; confirm another actor cannot answer it,webcan answer or decline it, and--waitreturns exit codes 0/2/3/4 for answered/declined/item timeout/local wait timeout.Evidence (Before & After)
Before: independently started agents had no durable shared task/question surface.
After: a real CLI smoke test completed task → concurrent cross-process claim → done and ask → answer → show using one named board. Only one of two concurrent claim processes succeeded. This evidence validates the Board contract; it does not demonstrate agent launch or message delivery.
Tested on
Environment
Node.js 22. After merging the latest
main,npm install/prepare/build completed successfully; focused Core and CLI tests passed (78 tests total), the Core build passed, and both package typechecks passed. The route clarification commit changes documentation only and passed the pre-commit formatting gate.Risk & Scope
Linked Issues
Tracking: #8724. The design from #9399 is included in this PR. The original #8724 Qwen-to-Qwen flow continues through #9576 and a sender/addressing follow-up rather than through this Board CLI.
中文说明
本 PR 做什么
本 PR 仅实现可移植的 Agent Board 层:通过显式的
--board与--as参数,让独立启动的 Qwen Code、Codex、脚本和定时任务共享任务与问答。它不实现成员、心跳、启动、唤醒、推送投递或运行时寻址,也不宣称完成 #8724 的 Qwen-to-Qwen 产品流程。存储层使用私有目录与文件、UUID、独占创建、进程内及跨进程锁、记录校验、原子更新和锁内 prune 复核。CLI 提供
show、task、claim、done、ask、answer、decline、prune与稳定 JSON 输出;等待问答可区分回答、拒绝、条目超时和本地等待超时。为什么需要
Agent Team 面向同一会话派生的 Qwen worker,#8724 面向已运行 Qwen Code 会话间的发现与消息。本 PR 补的是异构进程可共同使用的最小、持久、拉取式存储契约,而不是用户侧调度器。
Reviewer Test Plan
验证任务创建、认领、完成和问答的状态与退出码;验证畸形记录不会破坏健康记录或被目标 mutation 改写;验证 prune 与 reopen 竞争时会在锁内复核;验证私有权限、危险路径拒绝、并发唯一 ID 和完整可操作 ID 输出。
How to verify
按英文步骤用一个命名 board 完成 task → 并发 claim → done 与 ask → answer → show。预期两次并发 claim 只有一次成功,JSON 与人类输出保持一致。
Evidence
此前独立进程没有持久共享任务/问答表面;当前 smoke 已验证上述流程和并发 claim。该证据不证明 agent 启动或消息推送。
Tested on
macOS 已验证;Windows 原生 smoke 与最新 Linux CI 尚待验证。Node.js 22 环境下,合并 main 后 install/prepare/build、78 个聚焦测试、Core build 及两个 package typecheck 已通过;最后的路由澄清仅改文档并通过格式检查。
Risk & Scope
协作是自愿且拉取式的,actor 名称是标签而非认证。Qwen-to-Qwen 寻址、进程启动、Windows smoke、slash command、footer polling、fleet/tmux、推送、远程 board 和同 checkout 多写者均不在范围内。Agent Board 尚未发布,磁盘格式仍为实验性,无迁移要求。
Linked Issues
跟踪 #8724;#9399 的设计包含于本 PR。Qwen-to-Qwen 流程继续由 #9576 及 sender/addressing 后续推进。